UNPKG

20.5 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
217// ETA calculation
218class ETA {
219 constructor(capacity, initTime, initValue) {
220 // size of eta buffer
221 this.capacity = capacity || 100; // eta buffer with initial values
222
223 this.valueSeries = [initValue];
224 this.timeSeries = [initTime]; // eta time value
225
226 this.eta = '0';
227 } // add new values to calculation buffer
228
229
230 update(time, {
231 value,
232 total
233 }) {
234 this.valueSeries.push(value);
235 this.timeSeries.push(time); // trigger recalculation
236
237 this.calculate(total - value);
238 } // fetch estimated time
239
240
241 get value() {
242 return this.eta;
243 } // eta calculation - request number of remaining events
244
245
246 calculate(remaining) {
247 // get number of samples in eta buffer
248 const consumed = this.valueSeries.length;
249 const gap = Math.min(this.capacity, consumed);
250 const dValue = this.valueSeries[consumed - 1] - this.valueSeries[consumed - gap];
251 const dTime = this.timeSeries[consumed - 1] - this.timeSeries[consumed - gap]; // get progress per ms
252
253 const marginalRate = dValue / dTime; // strip past elements
254
255 this.valueSeries = this.valueSeries.slice(-this.capacity);
256 this.timeSeries = this.timeSeries.slice(-this.capacity); // eq: vt_rate *x = total
257
258 const eta = Math.ceil(remaining / marginalRate / 1000); // check values
259
260 return this.eta = isNaN(eta) ? 'NULL' : !isFinite(eta) ? 'INF' // +/- Infinity --- NaN already handled
261 : eta > 1e7 ? 'INF' // > 10M s ? - set upper display limit ~115days (1e7/60/60/24)
262 : eta < 0 ? 0 // negative ?
263 : eta; // assign
264 }
265
266}
267
268class State {
269 /** @type {number} the current bar value */
270 value = 0;
271 /** @type {number} the end value of the bar */
272
273 total = 100;
274 /** @type {?number} start time (used for eta calculation) */
275
276 start = null;
277 /** @type {?number} stop time (used for duration calculation) */
278
279 end = null;
280
281 constructor(total, value, payload, eta) {
282 var _eta$capacity;
283
284 this.value = value !== null && value !== void 0 ? value : 0;
285 this.total = total !== null && total !== void 0 ? total : 100;
286 this.start = Date.now(); // store start time for duration+eta calculation
287
288 this.end = null; // reset stop time for 're-start' scenario (used for duration calculation)
289
290 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
291
292 this.payload = payload !== null && payload !== void 0 ? payload : {};
293 return this;
294 }
295
296 static build(values) {
297 const {
298 total,
299 value,
300 payload,
301 eta
302 } = values;
303 return new State(total, value, payload, eta);
304 }
305
306 initialize(total, value, payload, eta) {
307 return Object.assign(this, new State(total, value, payload, eta));
308 }
309
310 get eta() {
311 return this.calETA.eta;
312 }
313
314 get reachLimit() {
315 return this.value >= this.total;
316 }
317
318 get progress() {
319 const progress = this.value / this.total;
320 return isNaN(progress) ? 0 // this.preset?.autoZero ? 0.0 : 1.0
321 : constraint(progress, 0, 1);
322 }
323
324 update(value, payload) {
325 var _this$calETA;
326
327 // if (payload) for (let key in payload) this[key] = payload[key]
328 this.value = value;
329 if (payload) this.payload = payload;
330 (_this$calETA = this.calETA) === null || _this$calETA === void 0 ? void 0 : _this$calETA.update(Date.now(), this); // add new value; recalculate eta
331
332 if (this.reachLimit) this.end = Date.now();
333 return this;
334 }
335
336 stop(value, payload) {
337 this.end = Date.now();
338 if (valid(value)) this.value = value;
339 if (payload) this.payload = payload;
340 }
341
342 get elapsed() {
343 var _this$end;
344
345 return round((((_this$end = this.end) !== null && _this$end !== void 0 ? _this$end : Date.now()) - this.start) / 1000);
346 }
347
348 get percent() {
349 return ~~(this.progress * 100);
350 }
351
352}
353
354class Escape {
355 constructor(conf) {
356 var _conf$ctx, _conf$arg, _conf$instant;
357
358 this.ctx = (_conf$ctx = conf.ctx) !== null && _conf$ctx !== void 0 ? _conf$ctx : {};
359 this.arg = (_conf$arg = conf.arg) !== null && _conf$arg !== void 0 ? _conf$arg : null;
360 this.fn = conf.fn.bind(this.ctx, this.arg);
361 this.instant = (_conf$instant = conf.instant) !== null && _conf$instant !== void 0 ? _conf$instant : true;
362 this.timer = null;
363 this.logs = [];
364 }
365
366 static build(conf) {
367 return new Escape(conf);
368 }
369
370 get active() {
371 return valid(this.timer);
372 }
373
374 loop(ms) {
375 if (typeof this.timer === OBJ) this.stop();
376 if (!this.fn) return void 0;
377
378 const func = () => {
379 this.fn();
380 this.logs.push(this.arg.map(state => state.eta));
381 };
382
383 if (this.instant) func();
384 this.timer = setInterval(func, ms);
385 }
386
387 stop() {
388 clearTimeout(this.timer);
389 return this.timer = null;
390 }
391
392}
393
394class Terminal {
395 stream = null;
396 /** @type {boolean} line wrapping enabled */
397
398 lineWrap = true;
399 /** @type {number} current, relative y position */
400
401 dy = 0;
402 /**
403 *
404 * @param {Config|object} configs
405 * @param {node::WriteStream} configs.stream
406 * @param {boolean} [configs.lineWrap]
407 * @param {number} [configs.dy]
408 *
409 */
410
411 constructor(configs) {
412 var _configs$lineWrap, _configs$dy;
413
414 this.stream = configs.stream;
415 this.lineWrap = (_configs$lineWrap = configs.lineWrap) !== null && _configs$lineWrap !== void 0 ? _configs$lineWrap : true;
416 this.dy = (_configs$dy = configs.dy) !== null && _configs$dy !== void 0 ? _configs$dy : 0;
417 } // tty environment ?
418
419
420 get isTTY() {
421 return this.stream.isTTY;
422 } // get terminal width
423
424
425 get width() {
426 var _this$stream$columns;
427
428 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
429 } // write content to output stream
430 // @TODO use string-width to strip length
431
432
433 write(tx) {
434 this.stream.write(tx); // this.stream.write(this.lineWrap ? tx.slice(0, this.width) : tx)
435 }
436
437 cleanWrite(tx) {
438 this.cursorTo(0, null); // set cursor to start of line
439
440 this.stream.write(tx); // write output
441
442 this.clearRight(); // clear to the right from cursor
443 } // save cursor position + settings
444
445
446 saveCursor() {
447 if (!this.isTTY) return void 0;
448 this.stream.write('\x1B7'); // save position
449 } // restore last cursor position + settings
450
451
452 restoreCursor() {
453 if (!this.isTTY) return void 0;
454 this.stream.write('\x1B8'); // restore cursor
455 } // show/hide cursor
456
457
458 showCursor(enabled) {
459 if (!this.isTTY) return void 0;
460 enabled ? this.stream.write('\x1B[?25h') : this.stream.write('\x1B[?25l');
461 } // change cursor position
462
463
464 cursorTo(x, y) {
465 if (!this.isTTY) return void 0;
466 readline.cursorTo(this.stream, x, y); // move cursor absolute
467 } // change relative cursor position
468
469
470 moveCursor(dx, dy) {
471 if (!this.isTTY) return void 0;
472 if (dy) this.dy += dy; // store current position
473
474 if (dx || dy) readline.moveCursor(this.stream, dx, dy); // move cursor relative
475 } // reset relative cursor
476
477
478 resetCursor() {
479 if (!this.isTTY) return void 0;
480 readline.moveCursor(this.stream, 0, -this.dy); // move cursor to initial line
481
482 readline.cursorTo(this.stream, 0, null); // first char
483
484 this.dy = 0; // reset counter
485 } // clear to the right from cursor
486
487
488 clearRight() {
489 if (!this.isTTY) return void 0;
490 readline.clearLine(this.stream, 1);
491 } // clear the full line
492
493
494 clearLine() {
495 if (!this.isTTY) return void 0;
496 readline.clearLine(this.stream, 0);
497 } // clear everything beyond the current line
498
499
500 clearDown() {
501 if (!this.isTTY) return void 0;
502 readline.clearScreenDown(this.stream);
503 } // add new line; increment counter
504
505
506 newline() {
507 this.stream.write('\n');
508 this.dy++;
509 } // control line wrapping
510
511
512 setLineWrap(enabled) {
513 if (!this.isTTY) return void 0;
514 this.lineWrap = enabled; // store state
515
516 enabled ? this.stream.write('\x1B[?7h') : this.stream.write('\x1B[?7l');
517 }
518
519}
520
521class Baro {
522 /** @type {Config|object} store config */
523 config;
524 /** @type {Terminal|object} store terminal instance */
525
526 terminal;
527 /** @type {boolean} progress bar active ? */
528
529 active = false;
530 /** @type {function} use default formatter or custom one ? */
531
532 formatter;
533 /** @type {[State]} */
534
535 states;
536 /**
537 *
538 * @param {Config} config
539 * @param {Layout} layout
540 */
541
542 constructor(config, layout) {
543 var _this$config$terminal;
544
545 this.config = config;
546 this.layout = layout;
547 this.states = [];
548 this.escape = Escape.build({
549 fn: this.#renderStates,
550 ctx: this,
551 arg: this.states
552 });
553 this.terminal = (_this$config$terminal = this.config.terminal) !== null && _this$config$terminal !== void 0 ? _this$config$terminal : new Terminal(this.config);
554 }
555
556 static build(config, layout) {
557 config = Config.build(config);
558 layout = Layout.build(layout);
559 return new Baro(config, layout);
560 }
561
562 get active() {
563 return !!this.escape.timer;
564 }
565
566 get forceRedraw() {
567 return this.config.forceRedraw || this.config.noTTYOutput && !this.terminal.isTTY; // force redraw in noTTY-mode!
568 }
569
570 get noTTY() {
571 return this.config.noTTYOutput && !this.terminal.isTTY;
572 }
573
574 boot() {
575 if (this.config.hideCursor) this.terminal.showCursor(false); // hide the cursor ?
576
577 if (!this.config.lineWrap) this.terminal.setLineWrap(false); // disable line wrapping ?
578
579 this.escape.loop(this.config.throttle); // initialize update timer
580 }
581 /**
582 * add a new bar to the stack
583 * @param total
584 * @param value
585 * @param payload
586 * @returns {State}
587 */
588
589
590 create(total, value, payload) {
591 // progress updates are only visible in TTY mode!
592 if (this.noTTY) return void 0;
593 const state = new State(total, value, payload, true);
594 state.last = Number.NEGATIVE_INFINITY;
595 this.states.push(state);
596 if (!this.escape.active && this.states.length) this.boot();
597 return state;
598 } // remove a bar from the stack
599
600
601 remove(state) {
602 const index = this.states.indexOf(state); // find element
603
604 if (index < 0) {
605 return false;
606 } // element found ?
607
608
609 this.states.splice(index, 1); // remove element
610
611 this.terminal.newline();
612 this.terminal.clearDown();
613 return true;
614 }
615
616 stop() {
617 this.escape.stop(); // stop timer
618
619 if (this.config.hideCursor) {
620 this.terminal.showCursor(true);
621 } // cursor hidden ?
622
623
624 if (!this.config.lineWrap) {
625 this.terminal.setLineWrap(true);
626 } // re-enable line wrapping ?
627
628
629 if (this.config.autoClear) {
630 this.terminal.resetCursor(); // reset cursor
631
632 this.terminal.clearDown();
633 } // clear all bars or show final progress
634 else {
635 for (let state of this.states) {
636 state.stop();
637 }
638
639 this.#renderStates(this.states);
640 }
641 }
642
643 #renderStates(states) {
644 this.terminal.resetCursor(); // reset cursor
645
646 for (let i = 0, hi = states.length; i < hi; i++) {
647 const state = states[i]; // update each bar
648
649 if (this.forceRedraw || state.value !== state.last) {
650 this.terminal.cleanWrite(this.layout.formatter(state));
651 } // string updated, only trigger redraw on change
652
653
654 this.terminal.newline();
655 state.last = state.value;
656 }
657
658 if (this.noTTY) {
659 this.terminal.newline();
660 this.terminal.newline();
661 } // add new line in noTTY mode
662
663
664 if (this.config.autoStop && states.every(state => state.reachLimit)) {
665 this.stop();
666 } // stop if autoStop and all bars stopped
667
668 }
669
670}
671
672export { Baro, Config, ETA, Escape, Layout, State };