UNPKG

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