bananass-utils-console 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,388 +0,0 @@
1
- /**
2
- * @fileoverview Console spinner.
3
- * @module bananass-utils-console/spinner
4
- * @license MIT Portions of this code were borrowed from [`yocto-spinner`](https://github.com/sindresorhus/yocto-spinner).
5
- */
6
-
7
- // --------------------------------------------------------------------------------
8
- // Import
9
- // --------------------------------------------------------------------------------
10
-
11
- import { styleText } from 'node:util';
12
- import isInteractive from '../is-interactive/index.js';
13
- import {
14
- successIcon,
15
- errorIcon,
16
- warningIcon,
17
- infoIcon,
18
- defaultSpinner,
19
- } from '../icons/index.js';
20
-
21
- // --------------------------------------------------------------------------------
22
- // Typedefs
23
- // --------------------------------------------------------------------------------
24
-
25
- /**
26
- * @typedef {"black"|"blackBright"|"blue"|"blueBright"|"cyan"|"cyanBright"|"gray"|"green"|"greenBright"|"grey"|"magenta"|"magentaBright"|"red"|"redBright"|"white"|"whiteBright"|"yellow"|"yellowBright"} ForegroundColors
27
- */
28
-
29
- /**
30
- * @typedef {object} SpinnerStyle
31
- * @property {string[]} frames
32
- * @property {number} interval
33
- */
34
-
35
- /**
36
- * @typedef {object} Options Spinner options.
37
- * @property {string | undefined} [text] Text to display next to the spinner. (default: `''`)
38
- * @property {ForegroundColors | undefined} [color] The color of the spinner. (default: `'yellow'`)
39
- * @property {NodeJS.WriteStream | undefined} [stream] The stream to which the spinner is written. (default: `process.stderr`)
40
- * @property {boolean | undefined} [isInteractive] Whether the spinner should be interactive. (default: Auto-detected)
41
- * @property {SpinnerStyle | undefined} [spinner]
42
- * Customize the spinner animation with a custom set of frames and interval.
43
- *
44
- * ```
45
- * {
46
- * frames: ['-', '\\', '|', '/'],
47
- * interval: 100,
48
- * }
49
- * ```
50
- *
51
- * Pass in any spinner from [`cli-spinners`](https://github.com/sindresorhus/cli-spinners).
52
- */
53
-
54
- // --------------------------------------------------------------------------------
55
- // Class
56
- // --------------------------------------------------------------------------------
57
-
58
- class Spinner {
59
- // ------------------------------------------------------------------------------
60
- // Private Properties
61
- // ------------------------------------------------------------------------------
62
-
63
- /** @type {SpinnerStyle['frames']} */
64
- #frames;
65
- /** @type {SpinnerStyle['interval']} */
66
- #interval;
67
- /** @type {number} */
68
- #currentFrame = -1;
69
- /** @type {NodeJS.Timeout | undefined} */
70
- #timer = undefined;
71
- /** @type {string} */
72
- #text;
73
- /** @type {NodeJS.WriteStream} */
74
- #stream;
75
- /** @type {ForegroundColors} */
76
- #color;
77
- /** @type {number} */
78
- #lines = 0;
79
- /** @type {boolean} */
80
- #isInteractive;
81
- /** @type {(signal: string) => void} */
82
- #exitHandlerBound;
83
- /** @type {number} */
84
- #lastSpinnerFrameTime = 0;
85
-
86
- /** @param {Options} options */
87
- constructor(options = {}) {
88
- const spinner = options.spinner ?? defaultSpinner;
89
-
90
- this.#frames = spinner.frames;
91
- this.#interval = spinner.interval;
92
- this.#text = options.text ?? '';
93
- this.#stream = options.stream ?? process.stderr;
94
- this.#color = options.color ?? 'yellow';
95
- this.#isInteractive = options.isInteractive ?? isInteractive(this.#stream);
96
- this.#exitHandlerBound = this.#exitHandler.bind(this);
97
- }
98
-
99
- // ------------------------------------------------------------------------------
100
- // Private Methods
101
- // ------------------------------------------------------------------------------
102
-
103
- /** @param {string} symbol @param {string} [text] */
104
- #symbolStop(symbol, text = this.#text) {
105
- return this.stop(`${symbol} ${text}`);
106
- }
107
-
108
- #render() {
109
- const currentTime = Date.now();
110
-
111
- // Ensure we only update the spinner frame at the wanted interval, even if the render method is called more often.
112
- if (
113
- this.#currentFrame === -1 ||
114
- currentTime - this.#lastSpinnerFrameTime >= this.#interval
115
- ) {
116
- this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
117
- this.#lastSpinnerFrameTime = currentTime;
118
- }
119
-
120
- const color = this.#color ?? 'yellow';
121
- const frame = this.#frames[this.#currentFrame];
122
- let string = `${styleText(color, frame)} ${this.#text}`;
123
-
124
- if (!this.#isInteractive) {
125
- string += '\n';
126
- }
127
-
128
- this.clear();
129
- this.#write(string);
130
-
131
- if (this.#isInteractive) {
132
- this.#lines = this.#lineCount(string);
133
- }
134
- }
135
-
136
- /** @param {string} text */
137
- #write(text) {
138
- this.#stream.write(text);
139
- }
140
-
141
- /** @param {string} text */
142
- #lineCount(text) {
143
- const width = this.#stream.columns ?? 80;
144
- const lines = text.split('\n');
145
-
146
- let lineCount = 0;
147
-
148
- for (const line of lines) {
149
- lineCount += Math.max(1, Math.ceil(line.length / width));
150
- }
151
-
152
- return lineCount;
153
- }
154
-
155
- #hideCursor() {
156
- if (this.#isInteractive) {
157
- this.#write('\u001B[?25l');
158
- }
159
- }
160
-
161
- #showCursor() {
162
- if (this.#isInteractive) {
163
- this.#write('\u001B[?25h');
164
- }
165
- }
166
-
167
- #subscribeToProcessEvents() {
168
- process.once('SIGINT', this.#exitHandlerBound);
169
- process.once('SIGTERM', this.#exitHandlerBound);
170
- }
171
-
172
- #unsubscribeFromProcessEvents() {
173
- process.off('SIGINT', this.#exitHandlerBound);
174
- process.off('SIGTERM', this.#exitHandlerBound);
175
- }
176
-
177
- /** @param {string} signal */
178
- #exitHandler(signal) {
179
- if (this.isSpinning) {
180
- this.stop();
181
- }
182
-
183
- // SIGINT: 128 + 2, SIGTERM: 128 + 15
184
- const exitCode = signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1;
185
- process.exit(exitCode);
186
- }
187
-
188
- // ------------------------------------------------------------------------------
189
- // Public Methods
190
- // ------------------------------------------------------------------------------
191
-
192
- /**
193
- * Starts the spinner.
194
- *
195
- * Optionally, updates the text.
196
- *
197
- * @param {string} [text] The text to display next to the spinner.
198
- * @returns The spinner instance.
199
- */
200
- start(text) {
201
- if (text) {
202
- this.#text = text;
203
- }
204
-
205
- if (this.isSpinning) {
206
- return this;
207
- }
208
-
209
- this.#hideCursor();
210
- this.#render();
211
- this.#subscribeToProcessEvents();
212
-
213
- this.#timer = setInterval(() => {
214
- this.#render();
215
- }, this.#interval);
216
-
217
- return this;
218
- }
219
-
220
- /**
221
- * Stops the spinner.
222
- *
223
- * Optionally displays a final message.
224
- *
225
- * @param {string} [finalText] The final text to display after stopping the spinner.
226
- * @returns The spinner instance.
227
- */
228
- stop(finalText) {
229
- if (!this.isSpinning) {
230
- return this;
231
- }
232
-
233
- clearInterval(this.#timer);
234
- this.#timer = undefined;
235
- this.#showCursor();
236
- this.clear();
237
- this.#unsubscribeFromProcessEvents();
238
-
239
- if (finalText) {
240
- this.#stream.write(`${finalText}\n`);
241
- }
242
-
243
- return this;
244
- }
245
-
246
- /**
247
- * Stops the spinner and displays a success symbol with the message.
248
- *
249
- * @param {string} [text] The success message to display.
250
- * @returns The spinner instance.
251
- */
252
- success(text) {
253
- return this.#symbolStop(successIcon, text);
254
- }
255
-
256
- /**
257
- * Stops the spinner and displays an error symbol with the message.
258
- *
259
- * @param {string} [text] The error message to display.
260
- * @returns The spinner instance.
261
- */
262
- error(text) {
263
- return this.#symbolStop(errorIcon, text);
264
- }
265
-
266
- /**
267
- * Stops the spinner and displays a warning symbol with the message.
268
- *
269
- * @param {string} [text] The warning message to display.
270
- * @returns The spinner instance.
271
- */
272
- warning(text) {
273
- return this.#symbolStop(warningIcon, text);
274
- }
275
-
276
- /**
277
- * Stops the spinner and displays an info symbol with the message.
278
- *
279
- * @param {string} [text] The info message to display.
280
- * @returns The spinner instance.
281
- */
282
- info(text) {
283
- return this.#symbolStop(infoIcon, text);
284
- }
285
-
286
- /**
287
- * Clears the spinner.
288
- *
289
- * @returns The spinner instance.
290
- */
291
- clear() {
292
- if (!this.#isInteractive) {
293
- return this;
294
- }
295
-
296
- this.#stream.cursorTo(0);
297
-
298
- for (let index = 0; index < this.#lines; index++) {
299
- if (index > 0) {
300
- this.#stream.moveCursor(0, -1);
301
- }
302
-
303
- this.#stream.clearLine(1);
304
- }
305
-
306
- this.#lines = 0;
307
-
308
- return this;
309
- }
310
-
311
- // ------------------------------------------------------------------------------
312
- // Getters and Setters
313
- // ------------------------------------------------------------------------------
314
-
315
- /**
316
- * Change the text displayed next to the spinner.
317
- *
318
- * @example
319
- * spinner.text = 'New text';
320
- */
321
- get text() {
322
- return this.#text;
323
- }
324
-
325
- /**
326
- * Change the text displayed next to the spinner.
327
- *
328
- * @example
329
- * spinner.text = 'New text';
330
- */
331
- set text(value) {
332
- this.#text = value ?? '';
333
- this.#render();
334
- }
335
-
336
- /**
337
- * Change the spinner color.
338
- */
339
- get color() {
340
- return this.#color;
341
- }
342
-
343
- /**
344
- * Change the spinner color.
345
- */
346
- set color(value) {
347
- this.#color = value;
348
- this.#render();
349
- }
350
-
351
- /**
352
- * Returns whether the spinner is currently spinning.
353
- */
354
- get isSpinning() {
355
- return this.#timer !== undefined;
356
- }
357
- }
358
-
359
- // --------------------------------------------------------------------------------
360
- // Export
361
- // --------------------------------------------------------------------------------
362
-
363
- /**
364
- * Create a new spinner instance.
365
- *
366
- * @param {Options} [options]
367
- * @returns {Spinner} A new spinner instance.
368
- *
369
- * @example
370
- * import createSpinner from 'bananass-utils-console/spinner';
371
- *
372
- * const spinner = createSpinner({
373
- * text: 'Loading…'
374
- * color: 'yellow',
375
- * stream: process.stderr,
376
- * spinner: {
377
- * frames: ['-', '\\', '|', '/'],
378
- * interval: 100,
379
- * },
380
- * }).start();
381
- *
382
- * setTimeout(() => {
383
- * spinner.success('Success!');
384
- * }, 2000);
385
- */
386
- export default function createSpinner(options) {
387
- return new Spinner(options);
388
- }
@@ -1 +0,0 @@
1
- export * from './theme.js';