UNPKG

11.2 kBJavaScriptView Raw
1'use strict';
2
3const styler = require('./style');
4const {
5 duration, formatNumber, stripAnsi, timestamp,
6} = require('./utils');
7
8const { cursorPosition, sequences } = require('./term');
9
10const spinners = require('./data/spinners.json');
11
12class ProgressBar {
13 constructor ({
14 total = 10, value = 0, format = '[$progress]', stream = process.stderr,
15 y, x = 0, width, complete = '=', incomplete = ' ', head = '>', clear,
16 interval, tokens = {}, spinner = 'dots', spinnerStyle, cursor = true,
17 durationOptions, formatOptions, onTick,
18 } = {}) {
19 this._total = total;
20 this._value = value;
21
22 this.width = width || Math.max(this._total, 60);
23
24 this.stream = stream;
25 this.cursor = cursor;
26 this.format = format;
27 this.characters = {
28 complete,
29 incomplete,
30 head,
31 };
32
33 this.clear = clear === undefined ? false : clear;
34 this.y = y || undefined;
35 this.x = x;
36
37 spinner = spinners[spinner] ? spinner : 'dots';
38
39 this.spinner = spinners[spinner].frames;
40 this.interval = interval || spinners[spinner].interval;
41 this.spinnerStyle = spinnerStyle;
42
43 this.durationOptions = durationOptions;
44 this.formatOptions = formatOptions;
45
46 this.tokens = tokens;
47 this.initialTokens = Object.assign({}, tokens);
48
49 this.complete = false;
50 this.lastUpdate = false;
51
52 this.ticks = 0;
53 this.start = timestamp();
54 this.tick = this.start;
55 this.eta = 0;
56
57 this.onTicks = onTick ? [ onTick ] : [ ];
58 Object.defineProperty(this, 'onTick', {
59 get () { return this.onTicks; },
60 set (tick) { this.onTicks.push(tick); },
61 enumerable: true,
62 configurable: true,
63 });
64
65 if (this._total === 0) {
66 this.complete = true;
67 } else {
68 this.render();
69 this.ticker = setInterval(() => {
70 if (!this.complete) {
71 this.ticks++;
72 if (this.ticks >= this.spinner.length) {
73 this.ticks = 0;
74 }
75
76 for (const tick of this.onTicks) {
77 if (typeof tick === 'function') {
78 tick();
79 }
80 }
81
82 this.render();
83 }
84 }, this.interval);
85 }
86 }
87
88 get value () {
89 return this._value;
90 }
91
92 set value (value) {
93 if (this.complete) {
94 return;
95 }
96
97 this._value = value;
98
99 this.tick = timestamp();
100 const elapsed = this.tick - this.start;
101 this.eta = elapsed / this._value * (this._total - this._value);
102
103 this.render();
104
105 if (this._value >= this._total) {
106 this.eta = 0;
107 this.done();
108 }
109 }
110
111 get total () {
112 return this._total;
113 }
114
115 set total (value) {
116 this._total = value;
117 this.render();
118 }
119
120 render () {
121 if (!this.stream.isTTY) {
122 return;
123 }
124
125 let ratio = this._value / this._total;
126 ratio = Math.min(Math.max(ratio, 0), 1);
127
128 const percent = Math.floor(ratio * 100);
129
130 const now = timestamp();
131 const elapsed = now - this.start;
132 const eta = this._value === 0 ? 0 : this.eta;
133 const rate = this._value / (elapsed / 1000);
134
135 const spinner = this.spinnerStyle ? styler(this.spinner[this.ticks], this.spinnerStyle) :
136 this.spinner[this.ticks];
137
138 let string = this.format.
139 replace(/\$value/g, formatNumber(this._value, this.formatOptions)).
140 replace(/\$total/g, formatNumber(this._total, this.formatOptions)).
141 replace(/\$remaining/g, formatNumber(this._total - this._value, this.formatOptions)).
142 replace(/\$elapsed/g, duration(elapsed, this.durationOptions)).
143 replace(/\$eta/g, isNaN(eta) || !isFinite(eta) || !eta ? 'unknown' : duration(eta, this.durationOptions)).
144 replace(/\$percent/g, `${ percent.toFixed(0) }%`).
145 replace(/\$rate/g, Math.round(rate)).
146 replace(/\$spinner/g, spinner).
147 replace(/\$(.+?)\b/g, (match, token) => {
148 if (this.tokens[token] !== undefined) {
149 let value = this.tokens[token];
150
151 if (typeof value === 'number') {
152 value = formatNumber(value, this.formatOptions);
153 }
154
155 return value;
156 }
157 if (token === 'progress') {
158 return '$progress';
159 }
160 return '';
161 });
162
163 const length = stripAnsi(string.replace(/\$progress/g, '')).length;
164 const columns = Math.max(0, this.stream.columns - length);
165 const width = Math.min(this.width, columns);
166
167 let completeLength = Math.max(0, Math.round(width * ratio));
168 let headLength = 0;
169 if (this._value < this._total && completeLength > 0 && this.characters.head) {
170 headLength = 1;
171 completeLength--;
172 }
173 const incompleteLength = Math.max(0, width - (completeLength + headLength));
174
175 const head = headLength ? this.characters.head : '';
176 const complete = this.characters.complete.repeat(completeLength);
177 const incomplete = this.characters.incomplete.repeat(incompleteLength);
178
179 string = string.replace('$progress', complete + head + incomplete).
180 replace(/\$progress/g, '');
181
182 if (this.lastUpdate !== string) {
183 this.stream.cursorTo(this.x, this.y);
184 if (this.cursor) {
185 this.stream.write('\x1b[?25l');
186 }
187 this.stream.write(string);
188 this.stream.clearLine(1);
189 this.lastUpdate = string;
190 }
191 }
192
193 done (text = '') {
194 if (this.complete) {
195 return;
196 }
197
198 this.complete = true;
199
200 clearInterval(this.ticker);
201
202 if (this.stream.isTTY) {
203 if (this.clear || text) {
204 if (this.stream.clearLine) {
205 this.stream.cursorTo(this.x, this.y);
206 this.stream.clearLine(1);
207 }
208 }
209 if (text) {
210 this.stream.write(text);
211 }
212 if (this.cursor) {
213 this.stream.write('\x1b[?25h');
214 }
215 } else if (text) {
216 console.log(text);
217 }
218 }
219
220 reset () {
221 this._value = 0;
222 this.complete = false;
223 this.start = timestamp();
224 Object.assign(this.tokens, this.initialTokens);
225 }
226}
227
228class Spinner {
229 constructor ({
230 spinner = 'dots', stream = process.stderr, x, y, interval, clear,
231 style, prepend = '', append = '', onTick, cursor = true,
232 } = {}) {
233 spinner = spinners[spinner] ? spinner : 'dots';
234
235 this.spinner = spinners[spinner];
236 this.interval = interval || this.spinner.interval;
237 this.frames = this.spinner.frames;
238
239 this.stream = stream;
240 this.cursor = cursor;
241 this.x = x;
242 this.y = y;
243
244 if (this.y !== undefined && this.x === undefined) {
245 this.x = 0;
246 }
247
248 this.style = style;
249
250 this._prepend = prepend;
251 this._append = append;
252
253 Object.defineProperty(this, 'prepend', {
254 get () { return this._prepend; },
255 set (string) { this._prepend = string; this.needsRedraw = 1; },
256 enumerable: true,
257 configurable: true,
258 });
259
260 Object.defineProperty(this, 'append', {
261 get () { return this._append; },
262 set (string) { this._append = string; this.needsRedraw = 1; },
263 enumerable: true,
264 configurable: true,
265 });
266
267 this.offset = stripAnsi(this.prepend).length;
268
269 this.clear = clear === undefined;
270
271 this.frame = 0;
272
273 this.running = false;
274
275 this.onTicks = onTick ? [ onTick ] : [ ];
276 Object.defineProperty(this, 'onTick', {
277 get () { return this.onTicks; },
278 set (tick) { this.onTicks.push(tick); },
279 enumerable: true,
280 configurable: true,
281 });
282 }
283
284 start () {
285 if (this.running) {
286 return;
287 }
288
289 this.running = true;
290
291 if (this.update) {
292 clearInterval(this.update);
293 }
294
295 this.needsRedraw = 1;
296
297 this.redraw = () => {
298 if (!this.stream.isTTY) {
299 return;
300 }
301
302 if (this.cursor) {
303 this.stream.write('\x1b[?25l');
304 }
305
306 if (this.x !== undefined) {
307 this.stream.cursorTo(this.x, this.y);
308 }
309 this.stream.write(`${ this.prepend } ${ this.append }`);
310 this.stream.moveCursor(this.append.length * -1);
311
312 this.needsRedraw = 0;
313 };
314
315 this.update = setInterval(() => {
316 for (const tick of this.onTicks) {
317 if (typeof tick === 'function') {
318 tick();
319 }
320 }
321
322 if (this.needsRedraw) {
323 this.redraw();
324 }
325
326 if (this.stream.isTTY) {
327 const character = this.style ? styler(this.frames[this.frame], this.style) :
328 this.frames[this.frame];
329
330 this.position();
331
332 if (this.cursor) {
333 this.stream.write('\x1b[?25l');
334 }
335 this.stream.write(character);
336
337 this.frame++;
338 if (this.frame >= this.frames.length) {
339 this.frame = 0;
340 }
341 }
342 }, this.interval);
343 }
344
345 position () {
346 if (this.stream.isTTY) {
347 if (this.x !== undefined) {
348 this.stream.cursorTo(this.x + this.offset, this.y);
349 } else {
350 this.stream.moveCursor(-1);
351 }
352 }
353 }
354
355 stop (text = '') {
356 if (!this.running) {
357 return;
358 }
359
360 this.running = false;
361
362 clearInterval(this.update);
363
364 if (this.stream.isTTY) {
365 if (this.clear || text) {
366 if (this.x !== undefined) {
367 this.stream.cursorTo(this.x, this.y);
368 } else {
369 this.position();
370 this.stream.moveCursor(this.offset * -1);
371 }
372 this.stream.clearLine(1);
373 }
374
375 if (text) {
376 this.stream.write(text);
377 }
378 if (this.cursor) {
379 this.stream.write('\x1b[?25h');
380 }
381 } else if (text) {
382 console.log(text);
383 }
384 }
385}
386
387class Stack {
388 constructor ({
389 rows = 1, stream = process.stderr, clear,
390 } = {}) {
391 this.rows = rows;
392 this.stream = stream;
393 this.clear = Boolean(clear);
394
395 this.slots = [];
396
397 this.y = 1;
398
399 this.slot = (i) => this.slots[i];
400 }
401
402 async start () {
403 if (this.stream.isTTY) {
404 this.stream.write(sequences.hideCursor());
405 this.stream.write('\n'.repeat(this.rows - 1));
406 const position = await cursorPosition();
407
408 this.y = position.y - this.rows;
409 } else {
410 this.y = this.rows;
411 }
412
413 for (let i = 0; i < this.rows; i++) {
414 const slot = {};
415 const y = this.y + i;
416
417 slot.progress = (options) => {
418 slot.progress = new ProgressBar(Object.assign(options, {
419 stream: this.stream,
420 cursor: false,
421 y,
422 }));
423 return slot.progress;
424 };
425
426 slot.spinner = (options) => {
427 slot.spinner = new Spinner(Object.assign(options, {
428 stream: this.stream,
429 cursor: false,
430 y,
431 }));
432 slot.spinner.start();
433 return slot.spinner;
434 };
435
436 this.slots[i] = slot;
437 }
438 }
439
440 stop () {
441 for (let i = 0; i < this.rows; i++) {
442 if (typeof this.slots[i].progress.done === 'function') {
443 this.slots[i].progress.done();
444 }
445 if (typeof this.slots[i].spinner.stop === 'function') {
446 this.slots[i].spinner.stop();
447 }
448 }
449
450 if (this.stream.isTTY) {
451 this.stream.write(sequences.showCursor());
452
453 if (this.clear) {
454 this.stream.cursorTo(0, this.y - 1);
455 this.stream.clearScreenDown();
456 } else {
457 this.stream.cursorTo(0, this.y + this.rows);
458 if (this.y + this.rows >= this.stream.rows) {
459 this.stream.write('\n');
460 }
461 }
462 }
463 }
464}
465
466module.exports = {
467 ProgressBar,
468 Spinner,
469 Stack,
470};