1 | 'use strict';
|
2 |
|
3 | const styler = require('./style');
|
4 | const {
|
5 | duration, formatNumber, stripAnsi, timestamp,
|
6 | } = require('./utils');
|
7 |
|
8 | const { cursorPosition, sequences } = require('./term');
|
9 |
|
10 | const spinners = require('./data/spinners.json');
|
11 |
|
12 | class 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 |
|
228 | class 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 |
|
387 | class 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('\n');
|
452 | this.stream.write(sequences.showCursor());
|
453 |
|
454 | if (this.clear) {
|
455 | this.stream.cursorTo(0, this.y - 1);
|
456 | this.stream.clearScreenDown();
|
457 | } else {
|
458 | this.stream.cursorTo(0, this.y + this.rows);
|
459 | if (this.y + this.rows >= this.stream.rows) {
|
460 | this.stream.write('\n');
|
461 | }
|
462 | }
|
463 | }
|
464 | }
|
465 | }
|
466 |
|
467 | module.exports = {
|
468 | ProgressBar,
|
469 | Spinner,
|
470 | Stack,
|
471 | };
|