1 | import { roundD2, round, constraint } from '@aryth/math';
|
2 | import { STR } from '@typen/enum-data-types';
|
3 | import { valid } from '@typen/nullish';
|
4 | import { setInterval } from 'timers/promises';
|
5 | import { IO as IO$1 } from '@arpel/backend';
|
6 | import { cursor, clear, decset } from '@arpel/escape';
|
7 | import { LF } from '@pres/enum-control-chars';
|
8 | import * as rl from 'readline';
|
9 |
|
10 | const trailZero = num => {
|
11 | if (!num) return '0';
|
12 | const tx = '' + roundD2(num);
|
13 | let i = tx.indexOf('.');
|
14 |
|
15 | if (!~i) {
|
16 | return tx + '.00';
|
17 | }
|
18 |
|
19 | const trail = tx.length - i;
|
20 |
|
21 | if (trail === 3) {
|
22 | return tx;
|
23 | }
|
24 |
|
25 | if (trail === 2) {
|
26 | return tx + '0';
|
27 | }
|
28 |
|
29 | if (trail === 1) {
|
30 | return tx + '00';
|
31 | }
|
32 |
|
33 | return tx;
|
34 | };
|
35 | const pad3 = tx => {
|
36 | const len = tx.length;
|
37 |
|
38 | if (len === 3) {
|
39 | return tx;
|
40 | }
|
41 |
|
42 | if (len === 2) {
|
43 | return ' ' + tx;
|
44 | }
|
45 |
|
46 | if (len === 1) {
|
47 | return ' ' + tx;
|
48 | }
|
49 |
|
50 | if (len === 0) {
|
51 | return ' ';
|
52 | }
|
53 |
|
54 | return tx;
|
55 | };
|
56 | const base3ToScale = (base3, dec) => {
|
57 | if (base3 === 0) return 'B';
|
58 |
|
59 | if (base3 === 1) return dec ? 'K' : 'KB';
|
60 |
|
61 | if (base3 === 2) return dec ? 'M' : 'MB';
|
62 |
|
63 | if (base3 === 3) return dec ? 'G' : 'GB';
|
64 |
|
65 | if (base3 === 4) return dec ? 'T' : 'TB';
|
66 |
|
67 | if (base3 === 5) return dec ? 'P' : 'PB';
|
68 |
|
69 | if (base3 === 6) return dec ? 'E' : 'EB';
|
70 |
|
71 | if (base3 === 7) return dec ? 'Z' : 'ZB';
|
72 |
|
73 | if (base3 === 8) return dec ? 'Y' : 'YB';
|
74 | };
|
75 |
|
76 | const DEFAULT_SENTENCE = 'progress [{bar}] {progress}% | ETA: {eta}s | {value}/{total}';
|
77 | class Layout {
|
78 | |
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | constructor(options) {
|
90 | const char = typeof options.char === STR ? [options.char, ' '] : Array.isArray(options.char) ? options.char : ['=', '-'];
|
91 | const [x, y] = char;
|
92 | this.size = options.size ?? 24;
|
93 | this.chars = [x.repeat(this.size + 1), y.repeat(this.size + 1)];
|
94 | this.glue = options.glue ?? '';
|
95 | this.autoZero = options.autoZero ?? false;
|
96 |
|
97 | this.sentence = options.sentence ?? DEFAULT_SENTENCE;
|
98 | if (options.bar) this.bar = options.bar.bind(this);
|
99 | if (options.degree) this.degree = options.degree.bind(this);
|
100 | if (options.format) this.format = options.format.bind(this);
|
101 | }
|
102 |
|
103 | static build(options) {
|
104 | return new Layout(options);
|
105 | }
|
106 |
|
107 | bar(state) {
|
108 | const {
|
109 | progress
|
110 | } = state;
|
111 | const {
|
112 | chars,
|
113 | glue,
|
114 | size
|
115 | } = this;
|
116 | const lenX = round(progress * size),
|
117 | lenY = size - lenX;
|
118 | const [x, y] = chars;
|
119 |
|
120 | return x.slice(0, lenX) + glue + y.slice(0, lenY);
|
121 | }
|
122 |
|
123 | get fullBar() {
|
124 | return this.chars[0].slice(0, this.size);
|
125 | }
|
126 |
|
127 | get zeroBar() {
|
128 | return this.chars[1].slice(0, this.size);
|
129 | }
|
130 |
|
131 | degree(state) {
|
132 | let {
|
133 | value,
|
134 | total
|
135 | } = state;
|
136 | const {
|
137 | base3 = true,
|
138 | decimal = false
|
139 | } = this;
|
140 | if (!base3) return `${round(value)}/${total}`;
|
141 | const thousand = decimal ? 1000 : 1024;
|
142 | let base3Level = 0;
|
143 |
|
144 | while (total > thousand) {
|
145 |
|
146 | total /= thousand;
|
147 | value /= thousand;
|
148 | base3Level++;
|
149 | }
|
150 |
|
151 | const t = trailZero(total);
|
152 | const v = trailZero(value).padStart(t.length);
|
153 |
|
154 | return `${v}/${t} ${base3ToScale(base3Level, decimal)}`;
|
155 | }
|
156 | |
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | format(state) {
|
164 | var _this$sentence;
|
165 |
|
166 | return (_this$sentence = this.sentence) === null || _this$sentence === void 0 ? void 0 : _this$sentence.replace(/\{(\w+)\}/g, (match, key) => {
|
167 | if (key === 'bar') return this.bar(state);
|
168 | if (key === 'degree') return this.degree(state);
|
169 | if (key in state) return state[key];
|
170 | return match;
|
171 | });
|
172 | }
|
173 |
|
174 | }
|
175 |
|
176 | class Config {
|
177 | |
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | constructor(config) {
|
205 | var _config$eta, _config$eta2;
|
206 |
|
207 |
|
208 |
|
209 | this.throttle = 1000 / (config.fps ?? 10);
|
210 |
|
211 | this.output = config.output ?? process.stdout;
|
212 | this.input = config.input ?? process.stdin;
|
213 | this.eta = config.eta ? {
|
214 | capacity: ((_config$eta = config.eta) === null || _config$eta === void 0 ? void 0 : _config$eta.capacity) ?? 10,
|
215 |
|
216 | autoUpdate: ((_config$eta2 = config.eta) === null || _config$eta2 === void 0 ? void 0 : _config$eta2.autoUpdate) ?? false
|
217 |
|
218 | } : null;
|
219 | this.terminal = config.terminal ?? null;
|
220 |
|
221 | this.autoClear = config.autoClear ?? false;
|
222 |
|
223 | this.autoStop = config.autoStop ?? false;
|
224 |
|
225 | this.hideCursor = config.hideCursor ?? true;
|
226 |
|
227 | this.lineWrap = config.lineWrap ?? false;
|
228 |
|
229 | this.syncUpdate = config.syncUpdate ?? true;
|
230 |
|
231 | this.noTTYOutput = config.noTTYOutput ?? false;
|
232 |
|
233 | this.notTTYSchedule = config.notTTYSchedule ?? 2000;
|
234 |
|
235 | this.forceRedraw = config.forceRedraw ?? false;
|
236 |
|
237 | return this;
|
238 | }
|
239 |
|
240 | static build(config) {
|
241 | return new Config(config);
|
242 | }
|
243 |
|
244 | }
|
245 |
|
246 |
|
247 | class ETA {
|
248 | constructor(capacity, initTime, initValue) {
|
249 |
|
250 | this.capacity = capacity || 100;
|
251 |
|
252 | this.valueSeries = [initValue];
|
253 | this.timeSeries = [initTime];
|
254 |
|
255 | this.estimate = '0';
|
256 | }
|
257 |
|
258 |
|
259 | update({
|
260 | now,
|
261 | value,
|
262 | total
|
263 | }) {
|
264 | this.valueSeries.push(value);
|
265 | this.timeSeries.push(now ?? Date.now());
|
266 | this.calculate(total - value);
|
267 | }
|
268 |
|
269 |
|
270 | calculate(remaining) {
|
271 | const len = this.valueSeries.length,
|
272 |
|
273 | cap = this.capacity,
|
274 | hi = len - 1,
|
275 | lo = len > cap ? len - cap : 0;
|
276 |
|
277 | const dValue = this.valueSeries[hi] - this.valueSeries[lo],
|
278 | dTime = this.timeSeries[hi] - this.timeSeries[lo],
|
279 | rate = dValue / dTime;
|
280 |
|
281 | if (len > cap) {
|
282 | this.valueSeries = this.valueSeries.slice(-cap);
|
283 | this.timeSeries = this.timeSeries.slice(-cap);
|
284 | }
|
285 |
|
286 |
|
287 | const eta = Math.ceil(remaining / (rate * 1000));
|
288 | return this.estimate = isNaN(eta) ? 'NULL' : !isFinite(eta) ? 'INF'
|
289 | : eta > 1e7 ? 'INF'
|
290 | : eta < 0 ? 0
|
291 | : eta;
|
292 | }
|
293 |
|
294 | }
|
295 |
|
296 | class State {
|
297 | constructor(data) {
|
298 | this.value = data.value ?? 0;
|
299 | this.total = data.total ?? 100;
|
300 | this.start = data.start ?? Date.now();
|
301 |
|
302 | this.end = data.end ?? null;
|
303 |
|
304 | this.calETA = data.eta ? new ETA(data.eta.capacity ?? 64, this.start, this.value) : null;
|
305 |
|
306 | return this;
|
307 | }
|
308 |
|
309 | static build(data) {
|
310 | return new State(data);
|
311 | }
|
312 |
|
313 | get eta() {
|
314 | var _this$calETA;
|
315 |
|
316 | return (_this$calETA = this.calETA) === null || _this$calETA === void 0 ? void 0 : _this$calETA.estimate;
|
317 | }
|
318 |
|
319 | get reachLimit() {
|
320 | return this.value >= this.total;
|
321 | }
|
322 |
|
323 | get elapsed() {
|
324 | return round(((this.end ?? Date.now()) - this.start) / 1000);
|
325 | }
|
326 |
|
327 | get percent() {
|
328 | return pad3('' + ~~(this.progress * 100));
|
329 | }
|
330 |
|
331 | get progress() {
|
332 | const progress = this.value / this.total;
|
333 | return isNaN(progress) ? 0
|
334 | : constraint(progress, 0, 1);
|
335 | }
|
336 |
|
337 | update(value) {
|
338 | var _this$calETA2;
|
339 |
|
340 | if (valid(value)) this.value = value;
|
341 | this.now = Date.now();
|
342 | (_this$calETA2 = this.calETA) === null || _this$calETA2 === void 0 ? void 0 : _this$calETA2.update(this);
|
343 |
|
344 | if (this.reachLimit) this.end = this.now;
|
345 | return this;
|
346 | }
|
347 |
|
348 | stop(value) {
|
349 | if (valid(value)) this.value = value;
|
350 | this.end = Date.now();
|
351 | return this;
|
352 | }
|
353 |
|
354 | }
|
355 |
|
356 | class Escape {
|
357 | constructor(conf) {
|
358 | this.ctx = conf.ctx ?? {};
|
359 | this.arg = conf.arg ?? null;
|
360 | this.fn = conf.fn.bind(this.ctx, this.arg);
|
361 | this.instant = conf.instant ?? true;
|
362 | this.on = false;
|
363 | }
|
364 |
|
365 | static build(conf) {
|
366 | return new Escape(conf);
|
367 | }
|
368 |
|
369 | get active() {
|
370 | return this.on;
|
371 | }
|
372 |
|
373 | async loop(ms) {
|
374 | this.on = true;
|
375 | if (!this.fn) return void 0;
|
376 | if (this.instant) this.fn();
|
377 |
|
378 | for await (const _ of setInterval(ms)) {
|
379 | if (!this.on) break;
|
380 | await this.fn();
|
381 | }
|
382 | }
|
383 |
|
384 | stop() {
|
385 | return this.on = false;
|
386 | }
|
387 |
|
388 | }
|
389 |
|
390 | class IO extends IO$1 {
|
391 |
|
392 | lineWrap = true;
|
393 |
|
394 |
|
395 | offset = 0;
|
396 | |
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 | constructor(configs) {
|
407 | super(configs);
|
408 | this.offset = configs.offset ?? 0;
|
409 |
|
410 | if (!this.isTTY) {
|
411 | console.error('>> [baro:IO] stream is not tty. baro:IO functions may not work.');
|
412 | }
|
413 | }
|
414 |
|
415 | static build(configs) {
|
416 | return new IO(configs);
|
417 | }
|
418 |
|
419 |
|
420 | get isTTY() {
|
421 | return this.output.isTTY;
|
422 | }
|
423 |
|
424 | get excessive() {
|
425 | return this.offset > this.height;
|
426 | }
|
427 |
|
428 |
|
429 |
|
430 |
|
431 | writeOff(text) {
|
432 | this.output.write(cursor.charTo(0) + text + clear.RIGHT_TO_CURSOR);
|
433 | }
|
434 |
|
435 | saveCursor() {
|
436 | this.output.write(cursor.SAVE);
|
437 | }
|
438 |
|
439 |
|
440 | restoreCursor() {
|
441 | this.output.write(cursor.RESTORE);
|
442 | }
|
443 |
|
444 |
|
445 | showCursor(enabled) {
|
446 | this.output.write(enabled ? cursor.SHOW : cursor.HIDE);
|
447 | }
|
448 |
|
449 |
|
450 | cursorTo(x, y) {
|
451 | this.output.write(cursor.goto(x, y));
|
452 | }
|
453 |
|
454 |
|
455 |
|
456 | moveCursor(dx, dy) {
|
457 | if (dy) this.offset += dy;
|
458 |
|
459 | if (dx || dy) rl.moveCursor(this.output, dx, dy);
|
460 | }
|
461 |
|
462 | offsetLines(offset) {
|
463 | if (offset === 0) return;
|
464 | const cursorMovement = offset >= 0 ? cursor.down(offset) : cursor.up(-offset);
|
465 | this.output.write(cursorMovement + cursor.charTo(0));
|
466 | }
|
467 |
|
468 |
|
469 | resetCursor() {
|
470 | this.offsetLines(-this.offset);
|
471 |
|
472 | this.offset = 0;
|
473 | }
|
474 |
|
475 | clearRight() {
|
476 | rl.clearLine(this.output, 1);
|
477 | }
|
478 |
|
479 |
|
480 | clearLine() {
|
481 | rl.clearLine(this.output, 0);
|
482 | }
|
483 |
|
484 |
|
485 | clearDown() {
|
486 | rl.clearScreenDown(this.output);
|
487 | }
|
488 |
|
489 |
|
490 |
|
491 | nextLine() {
|
492 | this.output.write(LF);
|
493 | this.offset++;
|
494 | }
|
495 |
|
496 |
|
497 | nextPage() {
|
498 | this.output.write(clear.ENTIRE_SCREEN + cursor.goto(0, 0));
|
499 | }
|
500 |
|
501 |
|
502 | setLineWrap(enabled) {
|
503 | this.output.write(enabled ? decset.WRAP_ON : decset.WRAP_OFF);
|
504 | }
|
505 |
|
506 | }
|
507 |
|
508 | class Baro {
|
509 | config;
|
510 | format;
|
511 | states = [];
|
512 | escape = Escape.build({
|
513 | fn: this.#renderStates,
|
514 | ctx: this,
|
515 | arg: this.states
|
516 | });
|
517 | #locker = null;
|
518 | |
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
524 | constructor(config, layout) {
|
525 | this.config = config;
|
526 | this.layout = layout;
|
527 | this.io = IO.build(this.config);
|
528 | }
|
529 |
|
530 | static build(config, layout) {
|
531 | config = Config.build(config);
|
532 | layout = Layout.build(layout);
|
533 | return new Baro(config, layout);
|
534 | }
|
535 |
|
536 | get active() {
|
537 | return this.escape.active;
|
538 | }
|
539 |
|
540 | get forceRedraw() {
|
541 | return this.config.forceRedraw || this.config.noTTYOutput && !this.io.isTTY;
|
542 | }
|
543 |
|
544 | async start() {
|
545 | const {
|
546 | io
|
547 | } = this;
|
548 | io.input.resume();
|
549 | if (this.config.hideCursor) io.showCursor(false);
|
550 |
|
551 | if (!this.config.lineWrap) io.setLineWrap(false);
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 | Promise.resolve().then(() => this.escape.loop(this.config.throttle));
|
565 | }
|
566 | |
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 | async append(state) {
|
574 | if (this.#locker) await this.#locker; // console.debug('>>', state.agent, 'waiting for occupy')
|
575 |
|
576 | const taskPromise = Promise.resolve().then(async () => {
|
577 | state.last = Number.NEGATIVE_INFINITY;
|
578 | this.states.push(state);
|
579 | if (!this.escape.active && this.states.length) await this.start();
|
580 | });
|
581 | this.#locker = taskPromise.then(() => this.#locker = null);
|
582 | return state;
|
583 | }
|
584 |
|
585 |
|
586 | remove(state) {
|
587 | const index = this.states.indexOf(state);
|
588 |
|
589 | if (index < 0) return false;
|
590 |
|
591 | this.states.splice(index, 1);
|
592 |
|
593 | this.io.nextLine();
|
594 | this.io.clearDown();
|
595 | return true;
|
596 | }
|
597 |
|
598 | async stop() {
|
599 | this.escape.stop();
|
600 |
|
601 | if (this.config.hideCursor) {
|
602 | this.io.showCursor(true);
|
603 | }
|
604 |
|
605 |
|
606 | if (!this.config.lineWrap) {
|
607 | this.io.setLineWrap(true);
|
608 | }
|
609 |
|
610 |
|
611 | if (this.config.autoClear) {
|
612 | this.io.resetCursor();
|
613 |
|
614 | this.io.clearDown();
|
615 | }
|
616 | else {
|
617 |
|
618 | await this.#renderStates(this.states);
|
619 | }
|
620 |
|
621 | this.io.input.pause();
|
622 | }
|
623 |
|
624 | async #renderStates(states) {
|
625 | const {
|
626 | io
|
627 | } = this;
|
628 | const height = io.height - 1;
|
629 |
|
630 | io.offsetLines(-Math.min(io.offset, height));
|
631 |
|
632 | io.offset = 0;
|
633 |
|
634 | if (height) {
|
635 | for (const state of states.slice(-height)) {
|
636 | if (this.forceRedraw || state.value !== state.last) {
|
637 |
|
638 | io.writeOff(this.layout.format(state));
|
639 | state.last = state.value;
|
640 | }
|
641 |
|
642 | io.nextLine();
|
643 | }
|
644 | }
|
645 |
|
646 | if (this.config.autoStop && states.every(state => state.reachLimit)) {
|
647 | await this.stop();
|
648 | }
|
649 |
|
650 | }
|
651 |
|
652 | }
|
653 |
|
654 | class Spin {
|
655 | max;
|
656 | value;
|
657 | width;
|
658 | step;
|
659 |
|
660 | constructor(max, width, step) {
|
661 | this.max = max;
|
662 | this.width = width;
|
663 | this.step = step;
|
664 | this.value = 0;
|
665 | }
|
666 |
|
667 | static build(max, width, step) {
|
668 | return new Spin(max, width, step);
|
669 | }
|
670 |
|
671 | next() {
|
672 | this.value += this.step;
|
673 | if (this.value >= this.max) this.value -= this.max;
|
674 | return this;
|
675 | }
|
676 |
|
677 | get sequel() {
|
678 | let next = this.value + this.width;
|
679 | if (next > this.max) next -= this.max;
|
680 | return next;
|
681 | }
|
682 |
|
683 | get record() {
|
684 | const {
|
685 | value,
|
686 | sequel
|
687 | } = this;
|
688 |
|
689 | if (value <= 0 || value >= this.max) {
|
690 | const x = this.width;
|
691 | const y = this.max - this.width;
|
692 | return [0, 0, x, y];
|
693 | } else {
|
694 | if (value < sequel) {
|
695 | const x = value - 1;
|
696 | const y = this.width;
|
697 | const z = this.max - this.width - value + 1;
|
698 | return [0, x, y, z];
|
699 | } else {
|
700 | const x = sequel;
|
701 | const y = value - sequel - 1;
|
702 | const z = this.max - value + 1;
|
703 | return [x, y, z, 0];
|
704 | }
|
705 | }
|
706 | }
|
707 |
|
708 | renderBar([bar, spc]) {
|
709 | const {
|
710 | value,
|
711 | sequel
|
712 | } = this;
|
713 |
|
714 | if (value <= 0 || value >= this.max) {
|
715 | const x = this.width;
|
716 | const y = this.max - this.width;
|
717 | return bar.slice(0, x) + spc.slice(0, y);
|
718 | } else {
|
719 | if (value < sequel) {
|
720 | const x = value;
|
721 | const y = this.width;
|
722 | const z = this.max - this.width - value;
|
723 | return spc.slice(0, x) + bar.slice(0, y) + spc.slice(0, z);
|
724 | } else {
|
725 | const x = sequel;
|
726 | const y = value - sequel;
|
727 | const z = this.max - value;
|
728 | return bar.slice(0, x) + spc.slice(0, y) + bar.slice(0, z);
|
729 | }
|
730 | }
|
731 | }
|
732 |
|
733 | }
|
734 |
|
735 | const CHARSET_SHADE = [`█`, `░`];
|
736 |
|
737 | const CHARSET_RECT = [`■`, ` `];
|
738 |
|
739 | const CHARSET_LEGACY = [`=`, `-`];
|
740 |
|
741 | export { Baro, CHARSET_LEGACY, CHARSET_RECT, CHARSET_SHADE, Config, ETA, Escape, Layout, Spin, State };
|