UNPKG

22.2 kBJavaScriptView Raw
1import { roundD2, round, constraint } from '@aryth/math';
2import { STR, OBJ } from '@typen/enum-data-types';
3import { valid } from '@typen/nullish';
4import readline from 'readline';
5
6const trailZero = num => {
7 if (!num) return '0';
8 const tx = '' + roundD2(num);
9 let i = tx.indexOf('.');
10
11 if (!~i) {
12 return tx + '.00';
13 }
14
15 let df = tx.length - i;
16
17 if (df === 3) {
18 return tx;
19 }
20
21 if (df === 2) {
22 return tx + '0';
23 }
24
25 if (df === 1) {
26 return tx + '00';
27 }
28
29 return tx;
30};
31const base3ToScale = (base3, dec) => {
32 if (base3 === 0) return 'B'; //
33
34 if (base3 === 1) return dec ? 'K' : 'KB'; // Kilo
35
36 if (base3 === 2) return dec ? 'M' : 'MB'; // Mega
37
38 if (base3 === 3) return dec ? 'G' : 'GB'; // Giga
39
40 if (base3 === 4) return dec ? 'T' : 'TB'; // Tera
41
42 if (base3 === 5) return dec ? 'P' : 'PB'; // Peta
43
44 if (base3 === 6) return dec ? 'E' : 'EB'; // Exa
45
46 if (base3 === 7) return dec ? 'Z' : 'ZB'; // Zetta
47
48 if (base3 === 8) return dec ? 'Y' : 'YB'; // Yotta
49};
50
51const DEFAULT_SENTENCE = 'progress [{bar}] {progress}% | ETA: {eta}s | {value}/{total}';
52class Layout {
53 /**
54 * @param {object} [options]
55 * @param {number} [options.size] size of the progressbar in chars
56 * @param {string[]|string} [options.char]
57 * @param {string} [options.glue]
58 * @param {string} [options.sentence]
59 * @param {boolean} [options.autoZero = false] autoZero - false
60 * @param {function(State):string} [options.bar]
61 * @param {function(State):string} [options.degree]
62 * @param {function(State,object):string} [options.formatter]
63 */
64 constructor(options) {
65 var _options$size, _options$glue, _options$autoZero, _options$sentence;
66
67 const char = typeof options.char === STR ? [options.char, ' '] : Array.isArray(options.char) ? options.char : ['=', '-'];
68 const [x, y] = char;
69 this.size = (_options$size = options.size) !== null && _options$size !== void 0 ? _options$size : 24;
70 this.chars = [x.repeat(this.size + 1), y.repeat(this.size + 1)];
71 this.glue = (_options$glue = options.glue) !== null && _options$glue !== void 0 ? _options$glue : '';
72 this.autoZero = (_options$autoZero = options.autoZero) !== null && _options$autoZero !== void 0 ? _options$autoZero : false; // autoZero - false
73
74 this.sentence = (_options$sentence = options.sentence) !== null && _options$sentence !== void 0 ? _options$sentence : DEFAULT_SENTENCE;
75 if (options.bar) this.bar = options.bar.bind(this);
76 if (options.degree) this.degree = options.degree.bind(this);
77 if (options.formatter) this.formatter = options.formatter.bind(this);
78 }
79
80 static build(options) {
81 return new Layout(options);
82 }
83
84 loadFormatter(formatter) {
85 this.formatter = formatter.bind(this);
86 return this;
87 }
88
89 bar(state) {
90 const {
91 progress
92 } = state;
93 const {
94 chars,
95 glue,
96 size
97 } = this;
98 const lenX = round(progress * size),
99 lenY = size - lenX;
100 const [x, y] = chars; // generate bar string by stripping the pre-rendered strings
101
102 return x.slice(0, lenX) + glue + y.slice(0, lenY);
103 }
104
105 degree(state) {
106 let {
107 value,
108 total
109 } = state;
110 const {
111 base3 = true,
112 decimal = true
113 } = this;
114 if (!base3) return `${round(value)}/${total}`;
115 const thousand = decimal ? 1000 : 1024;
116 let base3Level = 0;
117
118 while (total > thousand) {
119 // base3Level <= base3
120 total /= thousand;
121 value /= thousand;
122 base3Level++;
123 }
124
125 const totalText = trailZero(total);
126 const valueText = trailZero(value).padStart(totalText.length); // return { value: valueText, total: totalText, scale: base3ToScale(base3, dec) }
127
128 return `${valueText}/${totalText} ${base3ToScale(base3Level, decimal)}`;
129 }
130 /**
131 *
132 * @param {State|object} state
133 * @returns {string}
134 */
135
136
137 formatter(state) {
138 var _this$sentence;
139
140 return (_this$sentence = this.sentence) === null || _this$sentence === void 0 ? void 0 : _this$sentence.replace(/\{(\w+)\}/g, (match, key) => {
141 if (key === 'bar') return this.bar(state);
142 if (key === 'degree') return this.degree(state);
143 return key in state ? state[key] : match;
144 });
145 }
146
147}
148
149class Config {
150 /**
151 *
152 * @param {object} config
153 *
154 * @param {WriteStream} [config.stream = process.stderr] the output stream to write on
155 * @param {number} [config.fps = 12] the max update rate in fps (redraw will only triggered on value change)
156 * @param {Terminal|any} [config.terminal = null] external terminal provided ?
157 * @param {boolean} [config.autoClear = false] clear on finish ?
158 * @param {boolean} [config.autoStop = false] stop on finish ?
159 * @param {boolean} [config.hideCursor = false] hide the cursor ?
160 * @param {boolean} [config.lineWrap = false] allow or disable setLineWrap ?
161 * @param {string} [config.sentence = DEFAULT_FORMAT] the bar sentence
162 * @param {function} [config.formatTime = null] external time-sentence provided ?
163 * @param {function} [config.formatValue = null] external value-sentence provided ?
164 * @param {function} [config.formatBar = null] external bar-sentence provided ?
165 * @param {boolean} [config.syncUpdate = true] allow synchronous updates ?
166 * @param {boolean} [config.noTTYOutput = false] noTTY mode
167 * @param {number} [config.notTTYSchedule = 2000] schedule - 2s
168 * @param {boolean} [config.forceRedraw = false] force bar redraw even if progress did not change
169 *
170 * @param {object} [config.eta] eta config
171 * @param {boolean} [config.eta.on = false] switch to turn on eta
172 * @param {number} [config.eta.capacity = 10] the number of results to average ETA over
173 * @param {boolean} [config.eta.autoUpdate = false] automatic eta updates based on fps
174 * @returns {Config}
175 */
176 constructor(config) {
177 var _config$fps, _config$stream, _config$eta$capacity, _config$eta, _config$eta$autoUpdat, _config$eta2, _config$terminal, _config$autoClear, _config$autoStop, _config$hideCursor, _config$lineWrap, _config$syncUpdate, _config$noTTYOutput, _config$notTTYSchedul, _config$forceRedraw;
178
179 // merge layout
180 // the max update rate in fps (redraw will only triggered on value change)
181 this.throttle = 1000 / ((_config$fps = config.fps) !== null && _config$fps !== void 0 ? _config$fps : 10); // the output stream to write on
182
183 this.stream = (_config$stream = config.stream) !== null && _config$stream !== void 0 ? _config$stream : process.stderr;
184 this.eta = config.eta ? {
185 capacity: (_config$eta$capacity = (_config$eta = config.eta) === null || _config$eta === void 0 ? void 0 : _config$eta.capacity) !== null && _config$eta$capacity !== void 0 ? _config$eta$capacity : 10,
186 // the number of results to average ETA over
187 autoUpdate: (_config$eta$autoUpdat = (_config$eta2 = config.eta) === null || _config$eta2 === void 0 ? void 0 : _config$eta2.autoUpdate) !== null && _config$eta$autoUpdat !== void 0 ? _config$eta$autoUpdat : false // automatic eta updates based on fps
188
189 } : null;
190 this.terminal = (_config$terminal = config.terminal) !== null && _config$terminal !== void 0 ? _config$terminal : null; // external terminal provided ?
191
192 this.autoClear = (_config$autoClear = config.autoClear) !== null && _config$autoClear !== void 0 ? _config$autoClear : false; // clear on finish ?
193
194 this.autoStop = (_config$autoStop = config.autoStop) !== null && _config$autoStop !== void 0 ? _config$autoStop : false; // stop on finish ?
195
196 this.hideCursor = (_config$hideCursor = config.hideCursor) !== null && _config$hideCursor !== void 0 ? _config$hideCursor : false; // hide the cursor ?
197
198 this.lineWrap = (_config$lineWrap = config.lineWrap) !== null && _config$lineWrap !== void 0 ? _config$lineWrap : false; // disable setLineWrap ?
199
200 this.syncUpdate = (_config$syncUpdate = config.syncUpdate) !== null && _config$syncUpdate !== void 0 ? _config$syncUpdate : true; // allow synchronous updates ?
201
202 this.noTTYOutput = (_config$noTTYOutput = config.noTTYOutput) !== null && _config$noTTYOutput !== void 0 ? _config$noTTYOutput : false; // noTTY mode
203
204 this.notTTYSchedule = (_config$notTTYSchedul = config.notTTYSchedule) !== null && _config$notTTYSchedul !== void 0 ? _config$notTTYSchedul : 2000; // schedule - 2s
205
206 this.forceRedraw = (_config$forceRedraw = config.forceRedraw) !== null && _config$forceRedraw !== void 0 ? _config$forceRedraw : false; // force bar redraw even if progress did not change
207
208 return this;
209 }
210
211 static build(config) {
212 return new Config(config);
213 }
214
215}
216
217function _defineProperty(obj, key, value) {
218 if (key in obj) {
219 Object.defineProperty(obj, key, {
220 value: value,
221 enumerable: true,
222 configurable: true,
223 writable: true
224 });
225 } else {
226 obj[key] = value;
227 }
228
229 return obj;
230}
231
232function _classPrivateMethodGet(receiver, privateSet, fn) {
233 if (!privateSet.has(receiver)) {
234 throw new TypeError("attempted to get private field on non-instance");
235 }
236
237 return fn;
238}
239
240function _checkPrivateRedeclaration(obj, privateCollection) {
241 if (privateCollection.has(obj)) {
242 throw new TypeError("Cannot initialize the same private elements twice on an object");
243 }
244}
245
246function _classPrivateMethodInitSpec(obj, privateSet) {
247 _checkPrivateRedeclaration(obj, privateSet);
248
249 privateSet.add(obj);
250}
251
252// ETA calculation
253class ETA {
254 constructor(capacity, initTime, initValue) {
255 // size of eta buffer
256 this.capacity = capacity || 100; // eta buffer with initial values
257
258 this.valueSeries = [initValue];
259 this.timeSeries = [initTime]; // eta time value
260
261 this.eta = '0';
262 } // add new values to calculation buffer
263
264
265 update(time, {
266 value,
267 total
268 }) {
269 this.valueSeries.push(value);
270 this.timeSeries.push(time); // trigger recalculation
271
272 this.calculate(total - value);
273 } // fetch estimated time
274
275
276 get value() {
277 return this.eta;
278 } // eta calculation - request number of remaining events
279
280
281 calculate(remaining) {
282 // get number of samples in eta buffer
283 const consumed = this.valueSeries.length;
284 const gap = Math.min(this.capacity, consumed);
285 const dValue = this.valueSeries[consumed - 1] - this.valueSeries[consumed - gap];
286 const dTime = this.timeSeries[consumed - 1] - this.timeSeries[consumed - gap]; // get progress per ms
287
288 const marginalRate = dValue / dTime; // strip past elements
289
290 this.valueSeries = this.valueSeries.slice(-this.capacity);
291 this.timeSeries = this.timeSeries.slice(-this.capacity); // eq: vt_rate *x = total
292
293 const eta = Math.ceil(remaining / marginalRate / 1000); // check values
294
295 return this.eta = isNaN(eta) ? 'NULL' : !isFinite(eta) ? 'INF' // +/- Infinity --- NaN already handled
296 : eta > 1e7 ? 'INF' // > 10M s ? - set upper display limit ~115days (1e7/60/60/24)
297 : eta < 0 ? 0 // negative ?
298 : eta; // assign
299 }
300
301}
302
303class State {
304 /** @type {number} the current bar value */
305
306 /** @type {number} the end value of the bar */
307
308 /** @type {?number} start time (used for eta calculation) */
309
310 /** @type {?number} stop time (used for duration calculation) */
311 constructor(total, value, payload, eta) {
312 var _eta$capacity;
313
314 _defineProperty(this, "value", 0);
315
316 _defineProperty(this, "total", 100);
317
318 _defineProperty(this, "start", null);
319
320 _defineProperty(this, "end", null);
321
322 this.value = value !== null && value !== void 0 ? value : 0;
323 this.total = total !== null && total !== void 0 ? total : 100;
324 this.start = Date.now(); // store start time for duration+eta calculation
325
326 this.end = null; // reset stop time for 're-start' scenario (used for duration calculation)
327
328 this.calETA = eta ? new ETA((_eta$capacity = eta.capacity) !== null && _eta$capacity !== void 0 ? _eta$capacity : 64, this.start, this.value) : null; // initialize eta buffer
329
330 this.payload = payload !== null && payload !== void 0 ? payload : {};
331 return this;
332 }
333
334 static build(values) {
335 const {
336 total,
337 value,
338 payload,
339 eta
340 } = values;
341 return new State(total, value, payload, eta);
342 }
343
344 initialize(total, value, payload, eta) {
345 return Object.assign(this, new State(total, value, payload, eta));
346 }
347
348 get eta() {
349 return this.calETA.eta;
350 }
351
352 get reachLimit() {
353 return this.value >= this.total;
354 }
355
356 get progress() {
357 const progress = this.value / this.total;
358 return isNaN(progress) ? 0 // this.preset?.autoZero ? 0.0 : 1.0
359 : constraint(progress, 0, 1);
360 }
361
362 update(value, payload) {
363 var _this$calETA;
364
365 // if (payload) for (let key in payload) this[key] = payload[key]
366 this.value = value;
367 if (payload) this.payload = payload;
368 (_this$calETA = this.calETA) === null || _this$calETA === void 0 ? void 0 : _this$calETA.update(Date.now(), this); // add new value; recalculate eta
369
370 if (this.reachLimit) this.end = Date.now();
371 return this;
372 }
373
374 stop(value, payload) {
375 this.end = Date.now();
376 if (valid(value)) this.value = value;
377 if (payload) this.payload = payload;
378 }
379
380 get elapsed() {
381 var _this$end;
382
383 return round((((_this$end = this.end) !== null && _this$end !== void 0 ? _this$end : Date.now()) - this.start) / 1000);
384 }
385
386 get percent() {
387 return ~~(this.progress * 100);
388 }
389
390}
391
392class Escape {
393 constructor(conf) {
394 var _conf$ctx, _conf$arg, _conf$instant;
395
396 this.ctx = (_conf$ctx = conf.ctx) !== null && _conf$ctx !== void 0 ? _conf$ctx : {};
397 this.arg = (_conf$arg = conf.arg) !== null && _conf$arg !== void 0 ? _conf$arg : null;
398 this.fn = conf.fn.bind(this.ctx, this.arg);
399 this.instant = (_conf$instant = conf.instant) !== null && _conf$instant !== void 0 ? _conf$instant : true;
400 this.timer = null;
401 this.logs = [];
402 }
403
404 static build(conf) {
405 return new Escape(conf);
406 }
407
408 get active() {
409 return valid(this.timer);
410 }
411
412 loop(ms) {
413 if (typeof this.timer === OBJ) this.stop();
414 if (!this.fn) return void 0;
415
416 const func = () => {
417 this.fn();
418 this.logs.push(this.arg.map(state => state.eta));
419 };
420
421 if (this.instant) func();
422 this.timer = setInterval(func, ms);
423 }
424
425 stop() {
426 clearTimeout(this.timer);
427 return this.timer = null;
428 }
429
430}
431
432class Terminal {
433 /** @type {boolean} line wrapping enabled */
434
435 /** @type {number} current, relative y position */
436
437 /**
438 *
439 * @param {Config|object} configs
440 * @param {node::WriteStream} configs.stream
441 * @param {boolean} [configs.lineWrap]
442 * @param {number} [configs.dy]
443 *
444 */
445 constructor(configs) {
446 var _configs$lineWrap, _configs$dy;
447
448 _defineProperty(this, "stream", null);
449
450 _defineProperty(this, "lineWrap", true);
451
452 _defineProperty(this, "dy", 0);
453
454 this.stream = configs.stream;
455 this.lineWrap = (_configs$lineWrap = configs.lineWrap) !== null && _configs$lineWrap !== void 0 ? _configs$lineWrap : true;
456 this.dy = (_configs$dy = configs.dy) !== null && _configs$dy !== void 0 ? _configs$dy : 0;
457 } // tty environment ?
458
459
460 get isTTY() {
461 return this.stream.isTTY;
462 } // get terminal width
463
464
465 get width() {
466 var _this$stream$columns;
467
468 return (_this$stream$columns = this.stream.columns) !== null && _this$stream$columns !== void 0 ? _this$stream$columns : this.stream.isTTY ? 80 : 200; // set max width to 80 in tty-mode and 200 in noTTY-mode
469 } // write content to output stream
470 // @TODO use string-width to strip length
471
472
473 write(tx) {
474 this.stream.write(tx); // this.stream.write(this.lineWrap ? tx.slice(0, this.width) : tx)
475 }
476
477 cleanWrite(tx) {
478 this.cursorTo(0, null); // set cursor to start of line
479
480 this.stream.write(tx); // write output
481
482 this.clearRight(); // clear to the right from cursor
483 } // save cursor position + settings
484
485
486 saveCursor() {
487 if (!this.isTTY) return void 0;
488 this.stream.write('\x1B7'); // save position
489 } // restore last cursor position + settings
490
491
492 restoreCursor() {
493 if (!this.isTTY) return void 0;
494 this.stream.write('\x1B8'); // restore cursor
495 } // show/hide cursor
496
497
498 showCursor(enabled) {
499 if (!this.isTTY) return void 0;
500 enabled ? this.stream.write('\x1B[?25h') : this.stream.write('\x1B[?25l');
501 } // change cursor position
502
503
504 cursorTo(x, y) {
505 if (!this.isTTY) return void 0;
506 readline.cursorTo(this.stream, x, y); // move cursor absolute
507 } // change relative cursor position
508
509
510 moveCursor(dx, dy) {
511 if (!this.isTTY) return void 0;
512 if (dy) this.dy += dy; // store current position
513
514 if (dx || dy) readline.moveCursor(this.stream, dx, dy); // move cursor relative
515 } // reset relative cursor
516
517
518 resetCursor() {
519 if (!this.isTTY) return void 0;
520 readline.moveCursor(this.stream, 0, -this.dy); // move cursor to initial line
521
522 readline.cursorTo(this.stream, 0, null); // first char
523
524 this.dy = 0; // reset counter
525 } // clear to the right from cursor
526
527
528 clearRight() {
529 if (!this.isTTY) return void 0;
530 readline.clearLine(this.stream, 1);
531 } // clear the full line
532
533
534 clearLine() {
535 if (!this.isTTY) return void 0;
536 readline.clearLine(this.stream, 0);
537 } // clear everything beyond the current line
538
539
540 clearDown() {
541 if (!this.isTTY) return void 0;
542 readline.clearScreenDown(this.stream);
543 } // add new line; increment counter
544
545
546 newline() {
547 this.stream.write('\n');
548 this.dy++;
549 } // control line wrapping
550
551
552 setLineWrap(enabled) {
553 if (!this.isTTY) return void 0;
554 this.lineWrap = enabled; // store state
555
556 enabled ? this.stream.write('\x1B[?7h') : this.stream.write('\x1B[?7l');
557 }
558
559}
560
561var _renderStates = /*#__PURE__*/new WeakSet();
562
563class Baro {
564 /** @type {Config|object} store config */
565
566 /** @type {object} payload data */
567
568 /** @type {Terminal|object} store terminal instance */
569
570 /** @type {?string} last drawn string - only render on change! */
571
572 /** @type {number} last update time */
573
574 /** @type {boolean} progress bar active ? */
575
576 /** @type {function} use default formatter or custom one ? */
577
578 /** @type {State} */
579
580 /**
581 *
582 * @param {Config} config
583 * @param {Layout} layout
584 */
585 constructor(config, layout) {
586 var _this$config$terminal;
587
588 _classPrivateMethodInitSpec(this, _renderStates);
589
590 _defineProperty(this, "config", void 0);
591
592 _defineProperty(this, "payload", {});
593
594 _defineProperty(this, "terminal", void 0);
595
596 _defineProperty(this, "phrase", null);
597
598 _defineProperty(this, "prev", void 0);
599
600 _defineProperty(this, "active", false);
601
602 _defineProperty(this, "formatter", void 0);
603
604 _defineProperty(this, "state", void 0);
605
606 this.config = config;
607 this.layout = layout;
608 this.states = [];
609 this.escape = Escape.build({
610 fn: _classPrivateMethodGet(this, _renderStates, _renderStates2),
611 ctx: this,
612 arg: this.states
613 });
614 this.terminal = (_this$config$terminal = this.config.terminal) !== null && _this$config$terminal !== void 0 ? _this$config$terminal : new Terminal(this.config);
615 }
616
617 static build(config, layout) {
618 config = Config.build(config);
619 layout = Layout.build(layout);
620 return new Baro(config, layout);
621 }
622
623 get progress() {
624 return this.state.progress;
625 }
626
627 get forceRedraw() {
628 return this.config.forceRedraw || this.config.noTTYOutput && !this.terminal.isTTY; // force redraw in noTTY-mode!
629 }
630
631 get noTTY() {
632 return this.config.noTTYOutput && !this.terminal.isTTY;
633 }
634
635 boot() {
636 if (this.config.hideCursor) this.terminal.showCursor(false); // hide the cursor ?
637
638 if (!this.config.lineWrap) this.terminal.setLineWrap(false); // disable line wrapping ?
639
640 this.escape.loop(this.config.throttle); // initialize update timer
641
642 this.active = true;
643 } // add a new bar to the stack
644
645 /**
646 *
647 * @param total
648 * @param value
649 * @param payload
650 * @returns {State}
651 */
652
653
654 create(total, value, payload) {
655 // progress updates are only visible in TTY mode!
656 if (this.noTTY) return void 0;
657 const state = new State(total, value, payload, true);
658 state.last = Number.NEGATIVE_INFINITY;
659 this.states.push(state);
660 if (!this.escape.active && this.states.length) this.boot();
661 return state;
662 } // remove a bar from the stack
663
664
665 remove(state) {
666 const index = this.states.indexOf(state); // find element
667
668 if (index < 0) {
669 return false;
670 } // element found ?
671
672
673 this.states.splice(index, 1); // remove element
674
675 this.terminal.newline();
676 this.terminal.clearDown();
677 return true;
678 }
679
680 stop() {
681 this.escape.stop(); // stop timer
682
683 this.active = false; // set flag
684
685 if (this.config.hideCursor) {
686 this.terminal.showCursor(true);
687 } // cursor hidden ?
688
689
690 if (!this.config.lineWrap) {
691 this.terminal.setLineWrap(true);
692 } // re-enable line wrapping ?
693
694
695 if (this.config.autoClear) {
696 this.terminal.resetCursor(); // reset cursor
697
698 this.terminal.clearDown();
699 } // clear all bars or show final progress
700 else {
701 for (let state of this.states) {
702 state.stop();
703 }
704
705 _classPrivateMethodGet(this, _renderStates, _renderStates2).call(this, this.states);
706 }
707 }
708
709}
710
711function _renderStates2(states) {
712 this.terminal.resetCursor(); // reset cursor
713
714 for (let i = 0, hi = states.length; i < hi; i++) {
715 const state = states[i]; // update each bar
716
717 if (this.forceRedraw || state.value !== state.last) {
718 this.terminal.cleanWrite(this.layout.formatter(state));
719 } // string updated, only trigger redraw on change
720
721
722 this.terminal.newline();
723 state.last = state.value;
724 }
725
726 if (this.noTTY) {
727 this.terminal.newline();
728 this.terminal.newline();
729 } // add new line in noTTY mode
730
731
732 if (this.config.autoStop && states.every(state => state.reachLimit)) {
733 this.stop();
734 } // stop if autoStop and all bars stopped
735
736}
737
738export { Baro, Config, ETA, Escape, Layout, State };