'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var math = require('@aryth/math'); var enumDataTypes = require('@typen/enum-data-types'); var nullish = require('@typen/nullish'); var promises = require('timers/promises'); var backend = require('@arpel/backend'); var _escape = require('@arpel/escape'); var enumControlChars = require('@pres/enum-control-chars'); var rl = require('readline'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var rl__namespace = /*#__PURE__*/_interopNamespace(rl); const trailZero = num => { if (!num) return '0'; const tx = '' + math.roundD2(num); let i = tx.indexOf('.'); if (!~i) { return tx + '.00'; } const trail = tx.length - i; if (trail === 3) { return tx; } if (trail === 2) { return tx + '0'; } if (trail === 1) { return tx + '00'; } return tx; }; const pad3 = tx => { const len = tx.length; if (len === 3) { return tx; } if (len === 2) { return ' ' + tx; } if (len === 1) { return ' ' + tx; } if (len === 0) { return ' '; } return tx; }; const base3ToScale = (base3, dec) => { if (base3 === 0) return 'B'; // if (base3 === 1) return dec ? 'K' : 'KB'; // Kilo if (base3 === 2) return dec ? 'M' : 'MB'; // Mega if (base3 === 3) return dec ? 'G' : 'GB'; // Giga if (base3 === 4) return dec ? 'T' : 'TB'; // Tera if (base3 === 5) return dec ? 'P' : 'PB'; // Peta if (base3 === 6) return dec ? 'E' : 'EB'; // Exa if (base3 === 7) return dec ? 'Z' : 'ZB'; // Zetta if (base3 === 8) return dec ? 'Y' : 'YB'; // Yotta }; const DEFAULT_SENTENCE = 'progress [{bar}] {progress}% | ETA: {eta}s | {value}/{total}'; class Layout { /** * @param {object} [options] * @param {number} [options.size] size of the progressbar in chars * @param {string[]|string} [options.char] * @param {string} [options.glue] * @param {string} [options.sentence] * @param {boolean} [options.autoZero = false] autoZero - false * @param {function(State):string} [options.bar] * @param {function(State):string} [options.degree] * @param {function(State,object):string} [options.format] */ constructor(options) { const char = typeof options.char === enumDataTypes.STR ? [options.char, ' '] : Array.isArray(options.char) ? options.char : ['=', '-']; const [x, y] = char; this.size = options.size ?? 24; this.chars = [x.repeat(this.size + 1), y.repeat(this.size + 1)]; this.glue = options.glue ?? ''; this.autoZero = options.autoZero ?? false; // autoZero - false this.sentence = options.sentence ?? DEFAULT_SENTENCE; if (options.bar) this.bar = options.bar.bind(this); if (options.degree) this.degree = options.degree.bind(this); if (options.format) this.format = options.format.bind(this); } static build(options) { return new Layout(options); } bar(state) { const { progress } = state; const { chars, glue, size } = this; const lenX = math.round(progress * size), lenY = size - lenX; const [x, y] = chars; // generate bar string by stripping the pre-rendered strings return x.slice(0, lenX) + glue + y.slice(0, lenY); } get fullBar() { return this.chars[0].slice(0, this.size); } get zeroBar() { return this.chars[1].slice(0, this.size); } degree(state) { let { value, total } = state; const { base3 = true, decimal = false } = this; if (!base3) return `${math.round(value)}/${total}`; const thousand = decimal ? 1000 : 1024; let base3Level = 0; while (total > thousand) { // base3Level <= base3 total /= thousand; value /= thousand; base3Level++; } const t = trailZero(total); const v = trailZero(value).padStart(t.length); // return { value: valueText, total: totalText, scale: base3ToScale(base3, dec) } return `${v}/${t} ${base3ToScale(base3Level, decimal)}`; } /** * * @param {State|object} state * @returns {string} */ format(state) { var _this$sentence; return (_this$sentence = this.sentence) === null || _this$sentence === void 0 ? void 0 : _this$sentence.replace(/\{(\w+)\}/g, (match, key) => { if (key === 'bar') return this.bar(state); if (key === 'degree') return this.degree(state); if (key in state) return state[key]; return match; }); } } class Config { /** * * @param {object} config * * @param {ReadStream} [config.input = process.stdin] the output stream to read from * @param {WriteStream} [config.output = process.stdout] the output stream to write on * @param {number} [config.fps = 12] the max update rate in fps (redraw will only triggered on value change) * @param {IO|any} [config.terminal = null] external terminal provided ? * @param {boolean} [config.autoClear = false] clear on finish ? * @param {boolean} [config.autoStop = false] stop on finish ? * @param {boolean} [config.hideCursor = false] hide the cursor ? * @param {boolean} [config.lineWrap = false] allow or disable setLineWrap ? * @param {string} [config.sentence = DEFAULT_FORMAT] the bar sentence * @param {function} [config.formatTime = null] external time-sentence provided ? * @param {function} [config.formatValue = null] external value-sentence provided ? * @param {function} [config.formatBar = null] external bar-sentence provided ? * @param {boolean} [config.syncUpdate = true] allow synchronous updates ? * @param {boolean} [config.noTTYOutput = false] noTTY mode * @param {number} [config.notTTYSchedule = 2000] schedule - 2s * @param {boolean} [config.forceRedraw = false] force bar redraw even if progress did not change * * @param {object} [config.eta] eta config * @param {boolean} [config.eta.on = false] switch to turn on eta * @param {number} [config.eta.capacity = 10] the number of results to average ETA over * @param {boolean} [config.eta.autoUpdate = false] automatic eta updates based on fps * @returns {Config} */ constructor(config) { var _config$eta, _config$eta2; // merge layout // the max update rate in fps (redraw will only triggered on value change) this.throttle = 1000 / (config.fps ?? 10); // the output stream to write on this.output = config.output ?? process.stdout; this.input = config.input ?? process.stdin; this.eta = config.eta ? { capacity: ((_config$eta = config.eta) === null || _config$eta === void 0 ? void 0 : _config$eta.capacity) ?? 10, // the number of results to average ETA over autoUpdate: ((_config$eta2 = config.eta) === null || _config$eta2 === void 0 ? void 0 : _config$eta2.autoUpdate) ?? false // automatic eta updates based on fps } : null; this.terminal = config.terminal ?? null; // external terminal provided ? this.autoClear = config.autoClear ?? false; // clear on finish ? this.autoStop = config.autoStop ?? false; // stop on finish ? this.hideCursor = config.hideCursor ?? true; // hide the cursor ? this.lineWrap = config.lineWrap ?? false; // disable setLineWrap ? this.syncUpdate = config.syncUpdate ?? true; // allow synchronous updates ? this.noTTYOutput = config.noTTYOutput ?? false; // noTTY mode this.notTTYSchedule = config.notTTYSchedule ?? 2000; // schedule - 2s this.forceRedraw = config.forceRedraw ?? false; // force bar redraw even if progress did not change return this; } static build(config) { return new Config(config); } } // ETA: estimated time to completion class ETA { constructor(capacity, initTime, initValue) { // size of eta buffer this.capacity = capacity || 100; // eta buffer with initial values this.valueSeries = [initValue]; this.timeSeries = [initTime]; // eta time value this.estimate = '0'; } // add new values to calculation buffer update({ now, value, total }) { this.valueSeries.push(value); this.timeSeries.push(now ?? Date.now()); this.calculate(total - value); } // eta calculation - request number of remaining events calculate(remaining) { const len = this.valueSeries.length, // get number of samples in eta buffer cap = this.capacity, hi = len - 1, lo = len > cap ? len - cap : 0; // consumed-Math.min(cap,len) const dValue = this.valueSeries[hi] - this.valueSeries[lo], dTime = this.timeSeries[hi] - this.timeSeries[lo], rate = dValue / dTime; // get progress per ms if (len > cap) { this.valueSeries = this.valueSeries.slice(-cap); this.timeSeries = this.timeSeries.slice(-cap); } // strip past elements const eta = Math.ceil(remaining / (rate * 1000)); return this.estimate = isNaN(eta) ? 'NULL' : !isFinite(eta) ? 'INF' // +/- Infinity: NaN already handled : eta > 1e7 ? 'INF' // > 10M s: - set upper display limit ~115days (1e7/60/60/24) : eta < 0 ? 0 // negative: 0 : eta; // assign } } class State { constructor(data) { this.value = data.value ?? 0; this.total = data.total ?? 100; this.start = data.start ?? Date.now(); // store start time for duration+eta calculation this.end = data.end ?? null; // reset stop time for 're-start' scenario (used for duration calculation) this.calETA = data.eta ? new ETA(data.eta.capacity ?? 64, this.start, this.value) : null; // initialize eta buffer return this; } static build(data) { return new State(data); } get eta() { var _this$calETA; return (_this$calETA = this.calETA) === null || _this$calETA === void 0 ? void 0 : _this$calETA.estimate; } get reachLimit() { return this.value >= this.total; } get elapsed() { return math.round(((this.end ?? Date.now()) - this.start) / 1000); } get percent() { return pad3('' + ~~(this.progress * 100)); } get progress() { const progress = this.value / this.total; return isNaN(progress) ? 0 // this.preset?.autoZero ? 0.0 : 1.0 : math.constraint(progress, 0, 1); } update(value) { var _this$calETA2; if (nullish.valid(value)) this.value = value; this.now = Date.now(); (_this$calETA2 = this.calETA) === null || _this$calETA2 === void 0 ? void 0 : _this$calETA2.update(this); // add new value; recalculate eta if (this.reachLimit) this.end = this.now; return this; } stop(value) { if (nullish.valid(value)) this.value = value; this.end = Date.now(); return this; } } class Escape { constructor(conf) { this.ctx = conf.ctx ?? {}; this.arg = conf.arg ?? null; this.fn = conf.fn.bind(this.ctx, this.arg); this.instant = conf.instant ?? true; this.on = false; } static build(conf) { return new Escape(conf); } get active() { return this.on; } async loop(ms) { this.on = true; if (!this.fn) return void 0; if (this.instant) this.fn(); for await (const _ of promises.setInterval(ms)) { if (!this.on) break; await this.fn(); } } stop() { return this.on = false; } } class IO extends backend.IO { /** @type {boolean} line wrapping enabled */ lineWrap = true; /** @type {number} current, relative y position */ offset = 0; /** * * @param {Config|object} configs * @param {ReadStream} configs.input * @param {WriteStream} configs.output * @param {boolean} [configs.lineWrap] * @param {number} [configs.offset] * */ constructor(configs) { super(configs); this.offset = configs.offset ?? 0; if (!this.isTTY) { console.error('>> [baro:IO] stream is not tty. baro:IO functions may not work.'); } } static build(configs) { return new IO(configs); } // tty environment get isTTY() { return this.output.isTTY; } get excessive() { return this.offset > this.height; } // CSI n G CHA Cursor Horizontal Absolute Moves the cursor to column n (default 1). // write output // clear to the right from cursor writeOff(text) { this.output.write(_escape.cursor.charTo(0) + text + _escape.clear.RIGHT_TO_CURSOR); } saveCursor() { this.output.write(_escape.cursor.SAVE); } // save cursor position + settings restoreCursor() { this.output.write(_escape.cursor.RESTORE); } // restore last cursor position + settings showCursor(enabled) { this.output.write(enabled ? _escape.cursor.SHOW : _escape.cursor.HIDE); } // show/hide cursor cursorTo(x, y) { this.output.write(_escape.cursor.goto(x, y)); } // change cursor position: rl.cursorTo(this.output, x, y) // change relative cursor position moveCursor(dx, dy) { if (dy) this.offset += dy; // store current position if (dx || dy) rl__namespace.moveCursor(this.output, dx, dy); // move cursor relative } offsetLines(offset) { if (offset === 0) return; const cursorMovement = offset >= 0 ? _escape.cursor.down(offset) : _escape.cursor.up(-offset); this.output.write(cursorMovement + _escape.cursor.charTo(0)); // rl.moveCursor(this.output, -this.width, offset) // move cursor to initial line // rl.cursorTo(this.stream, 0, null) // first char } // reset relative cursor resetCursor() { this.offsetLines(-this.offset); // rl.moveCursor(this.output, -this.width, this.offset) // move cursor to initial line // rl.cursorTo(this.stream, 0, null) // first char this.offset = 0; // reset counter } clearRight() { rl__namespace.clearLine(this.output, 1); } // clear to the right from cursor clearLine() { rl__namespace.clearLine(this.output, 0); } // clear the full line clearDown() { rl__namespace.clearScreenDown(this.output); } // clear everything beyond the current line // add new line; increment counter nextLine() { this.output.write(enumControlChars.LF); this.offset++; } // create new page, equivalent to Ctrl+L clear screen nextPage() { this.output.write(_escape.clear.ENTIRE_SCREEN + _escape.cursor.goto(0, 0)); // this.offset = 0 } // store state + control line wrapping setLineWrap(enabled) { this.output.write(enabled ? _escape.decset.WRAP_ON : _escape.decset.WRAP_OFF); } } class Baro { config; format; states = []; escape = Escape.build({ fn: this.#renderStates, ctx: this, arg: this.states }); #locker = null; /** * * @param {Config} config * @param {Layout} layout */ constructor(config, layout) { this.config = config; this.layout = layout; this.io = IO.build(this.config); } static build(config, layout) { config = Config.build(config); layout = Layout.build(layout); return new Baro(config, layout); } get active() { return this.escape.active; } get forceRedraw() { return this.config.forceRedraw || this.config.noTTYOutput && !this.io.isTTY; // force redraw in noTTY-mode! } async start() { const { io } = this; io.input.resume(); if (this.config.hideCursor) io.showCursor(false); // hide the cursor ? if (!this.config.lineWrap) io.setLineWrap(false); // disable line wrapping ? // this.io.output.write('\f') // const height = this.io.height // this.io.output.write(cursor.nextLine(height) + cursor.prevLine(height)) // this.io.output.write(scroll.down(height)) // const [ x, y ] = await io.asyncCursorPos() // console.log('x', x, 'y', y, 'offset', io.offset, 'states', this.states.length, 'height', io.height) // if (x + this.states.length >= io.height) { // io.output.write('\f') // // io.nextPage() // } // WARNING: intentionally call loop without await Promise.resolve().then(() => this.escape.loop(this.config.throttle)); // initialize update timer } /** * add a new bar to the stack * @param {State|object} state // const state = new State(total, value, this.config.eta) * @returns {State|object} */ async append(state) { if (this.#locker) await this.#locker; // console.debug('>>', state.agent, 'waiting for occupy') const taskPromise = Promise.resolve().then(async () => { state.last = Number.NEGATIVE_INFINITY; this.states.push(state); if (!this.escape.active && this.states.length) await this.start(); }); this.#locker = taskPromise.then(() => this.#locker = null); return state; } // remove a bar from the stack remove(state) { const index = this.states.indexOf(state); // find element if (index < 0) return false; // element found ? this.states.splice(index, 1); // remove element this.io.nextLine(); this.io.clearDown(); return true; } async stop() { this.escape.stop(); // stop timer if (this.config.hideCursor) { this.io.showCursor(true); } // cursor hidden ? if (!this.config.lineWrap) { this.io.setLineWrap(true); } // re-enable line wrapping ? if (this.config.autoClear) { this.io.resetCursor(); // reset cursor this.io.clearDown(); } // clear all bars or show final progress else { // for (let state of this.states) { state.stop() } await this.#renderStates(this.states); } this.io.input.pause(); } async #renderStates(states) { const { io } = this; const height = io.height - 1; // const [ x, y ] = await io.asyncCursorPos() io.offsetLines(-Math.min(io.offset, height)); // reset cursor io.offset = 0; if (height) { for (const state of states.slice(-height)) { if (this.forceRedraw || state.value !== state.last) { // `CURSOR (${x}, ${y}) OFFSET (${io.offset}) TERM (${io.size}) ` + io.writeOff(this.layout.format(state)); state.last = state.value; } io.nextLine(); } } if (this.config.autoStop && states.every(state => state.reachLimit)) { await this.stop(); } // stop if autoStop and all bars stopped } } class Spin { max; value; width; step; constructor(max, width, step) { this.max = max; this.width = width; this.step = step; this.value = 0; } static build(max, width, step) { return new Spin(max, width, step); } next() { this.value += this.step; if (this.value >= this.max) this.value -= this.max; return this; } get sequel() { let next = this.value + this.width; if (next > this.max) next -= this.max; return next; } get record() { const { value, sequel } = this; if (value <= 0 || value >= this.max) { const x = this.width; const y = this.max - this.width; return [0, 0, x, y]; } else { if (value < sequel) { const x = value - 1; const y = this.width; const z = this.max - this.width - value + 1; return [0, x, y, z]; } else { const x = sequel; const y = value - sequel - 1; const z = this.max - value + 1; return [x, y, z, 0]; } } } renderBar([bar, spc]) { const { value, sequel } = this; if (value <= 0 || value >= this.max) { const x = this.width; const y = this.max - this.width; return bar.slice(0, x) + spc.slice(0, y); } else { if (value < sequel) { const x = value; const y = this.width; const z = this.max - this.width - value; return spc.slice(0, x) + bar.slice(0, y) + spc.slice(0, z); } else { const x = sequel; const y = value - sequel; const z = this.max - value; return bar.slice(0, x) + spc.slice(0, y) + bar.slice(0, z); } } } } const CHARSET_SHADE = [`█`, `░`]; // \u2588 \u2591 const CHARSET_RECT = [`■`, ` `]; // \u25A0 const CHARSET_LEGACY = [`=`, `-`]; exports.Baro = Baro; exports.CHARSET_LEGACY = CHARSET_LEGACY; exports.CHARSET_RECT = CHARSET_RECT; exports.CHARSET_SHADE = CHARSET_SHADE; exports.Config = Config; exports.ETA = ETA; exports.Escape = Escape; exports.Layout = Layout; exports.Spin = Spin; exports.State = State;