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