UNPKG

multi-progress-bars

Version:

Multiple progress bars with option for indefinite spinners

795 lines (783 loc) 33.7 kB
'use strict'; var chalk = require('chalk'); var path = require('path'); var stringWidth = require('string-width'); var stripAnsi = require('strip-ansi'); var util = require('util'); function _interopNamespaceDefault(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 path__namespace = /*#__PURE__*/_interopNamespaceDefault(path); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } const ESC = '\x1B'; const CSI = ESC + '['; const numberTo1StringHelper = (number) => (number !== undefined) ? (number + 1).toFixed(0) : ''; /** CUrsor Position * * @param row (required) 0-index absolute row * @param column (optional) 0-index absolute column */ const CUP = (row, column) => `${CSI}${numberTo1StringHelper(row)};${numberTo1StringHelper(column)}H`; var EL_MODE; (function (EL_MODE) { EL_MODE[EL_MODE["TO_END"] = 0] = "TO_END"; EL_MODE[EL_MODE["TO_BEGINNING"] = 1] = "TO_BEGINNING"; EL_MODE[EL_MODE["ENTIRE_LINE"] = 2] = "ENTIRE_LINE"; })(EL_MODE || (EL_MODE = {})); /** Erase Line * * @param mode EL_MODE.TO_END, .TO_BEGINNING, or .ENTIRE_LINE */ const EL = (mode = EL_MODE.TO_END) => { return `${CSI}${mode.toString()}K`; }; var ED_MODE; (function (ED_MODE) { ED_MODE[ED_MODE["TO_END"] = 0] = "TO_END"; ED_MODE[ED_MODE["TO_BEGINNING"] = 1] = "TO_BEGINNING"; ED_MODE[ED_MODE["ENTIRE_SCREEN"] = 2] = "ENTIRE_SCREEN"; ED_MODE[ED_MODE["ENTIRE_SCREEN_DELETE_SCROLLBACK"] = 3] = "ENTIRE_SCREEN_DELETE_SCROLLBACK"; })(ED_MODE || (ED_MODE = {})); /** Erase Display * * @param mode ED_MORE.TO_END, .TO_BEGINNING, ENTIRE_SCREEN, or .ENTIRE_SCREEN_DELETE_SCROLLBACK */ const ED = (mode = ED_MODE.TO_END) => { return `${CSI}${mode.toString()}J`; }; // Anyways, probably don't want any styling codes to linger past one line. const clampString = (message, width) => { while (stringWidth(message) > width) { // Can't be sure we are slicing off a character vs a control sequence or colors // so do it this way, checking each time. message = message.slice(0, message.length - 1); } return message; }; // Split by newlines, and then split the resulting lines if they run longer than width. const splitLinesAndClamp = (writeString, maxWidth) => { return writeString.split(/\r?\n/g).reduce((prev, curr) => { const clamped = []; do { let width = curr.length; let front = curr; while (stringWidth(front) > maxWidth) { front = curr.slice(0, width); width--; } curr = curr.slice(width); clamped.push(front); } while (curr.length > 0); return [...prev, ...clamped]; }, []); }; class VirtualConsole { constructor(options) { var _a; this.originalConsole = console; this.stream = options.stream; this.width = this.stream.columns; this.height = this.stream.rows; this.stream.on('resize', () => { this.resize(); }); this.progressHeight = 0; this.progressBuffer = []; this.consoleBuffer = []; this.consoleHeight = this.height; const anchor = options.anchor || 'top'; this.getOutString = (anchor === 'top') ? this.getOutStringTop : this.getOutStringBottom; if (!process.stdout.isTTY) { this.log = console.log; } this.warn = this.log; this.error = this.log; console = this; this.init(); (_a = this.refresh) === null || _a === void 0 ? void 0 : _a.call(this); } init() { var _a; const blank = '\n'.repeat(this.stream.rows) + CUP(0) + ED(ED_MODE.TO_END); (_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(blank); } checkConsoleIntercept() { if (!this.originalConsole) { this.originalConsole = console; console = this; } } resize() { var _a; this.width = this.stream.columns; this.height = this.stream.rows; this.progressHeight = Math.min(this.progressHeight, this.height); this.consoleHeight = this.height - this.progressHeight; (_a = this.refresh) === null || _a === void 0 ? void 0 : _a.call(this); } done() { var _a; if (this.progressBuffer.length > this.height) { this.dumpProgressBuffer(); } else { (_a = this.stream) === null || _a === void 0 ? void 0 : _a.write('\x1b[0m\n'); } console = this.originalConsole; this.originalConsole = null; } /* Prints out the buffers as they are */ refresh() { var _a; // pop top of consoleBuffer if longer than consoleHeight const topLines = (this.consoleBuffer.length > this.consoleHeight) ? this.consoleBuffer.splice(0, this.consoleBuffer.length - this.consoleHeight) : []; // If progress buffer is larger than screen height - borders, then truncate top const bufferStartIndex = Math.max(this.progressBuffer.length - this.currentHeightMinusBorders(), 0); (_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(CUP(0) + this.getOutString(bufferStartIndex, topLines)); } getOutStringTop(bufferStartIndex, topLines) { const fillerCount = Math.max(0, this.consoleHeight - this.consoleBuffer.length); return [ topLines.map((val) => val + EL(EL_MODE.TO_END)).join('\n'), ((this.topBorder === undefined) ? // Top border or null null : (this.topBorder + EL(EL_MODE.TO_END))), this.progressBuffer // Progress bars .slice(bufferStartIndex) .map((val) => val + EL(EL_MODE.TO_END)) .join('\n'), ((this.bottomBorder === undefined) ? // Bottom border or null null : (this.bottomBorder + EL(EL_MODE.TO_END))), (this.consoleBuffer.length) ? this.consoleBuffer.map((val) => val + EL(EL_MODE.TO_END)).join('\n') + ED(ED_MODE.TO_END) : null, (fillerCount) ? (new Array(fillerCount).fill(EL(EL_MODE.ENTIRE_LINE))).join('\n') : null, // Empty space between end of buffer and actual end ].filter((v) => { return (v !== undefined) && (v !== '') && (v !== null); // Remove falsey/empty values }).join('\n'); // Join with newlines } getOutStringBottom(bufferStartIndex, topLines) { const fillerCount = Math.max(0, this.consoleHeight - this.consoleBuffer.length); return [ topLines.map((val) => val + EL(EL_MODE.TO_END)).join('\n'), this.consoleBuffer.map((val) => val + EL(EL_MODE.TO_END)).join('\n'), (fillerCount) ? (new Array(fillerCount).fill(EL(EL_MODE.ENTIRE_LINE))).join('\n') : null, ((this.topBorder === undefined) ? // Top border or null null : (this.topBorder + EL(EL_MODE.TO_END))), this.progressBuffer // Progress bars or [] .slice(bufferStartIndex) .map((val) => val + EL(EL_MODE.TO_END)) .join('\n'), ((this.bottomBorder === undefined) ? // Bottom border or null null : (this.bottomBorder + EL(EL_MODE.TO_END))), ].filter((v) => { return (v !== undefined) && (v !== '') && (v !== null); // Remove falsey/empty values }).join('\n'); // Join with newlines } log(...data) { // Format incoming strings and split into lines and clamp. if (data.length !== 0) { const writeString = util.format(...data); const clampedLines = splitLinesAndClamp(writeString, this.width); this.consoleBuffer.push(...clampedLines); } this.refresh(); } /** Add or Update Progress Entry * * @param options * index: number * data: string */ upsertProgress(options) { // Reactivate console intercepting this.checkConsoleIntercept(); const numToExtend = 1 + options.index - this.progressBuffer.length; // Truncate progress line to console width. this.progressBuffer[options.index] = clampString(options.data, this.width); // If we're not increasing the progress bars section, we're done. if (numToExtend <= 0) { return; } // Extend the progress bars section, and reduce the corresponding console buffer height. this.progressHeight = Math.min(this.progressHeight + numToExtend, this.height); this.consoleHeight = Math.max(this.consoleHeight - numToExtend, 0); } setTopBorder(border) { if (this.topBorder === undefined) { this.progressHeight = Math.min(this.height, this.progressHeight + 1); this.consoleHeight = this.height - this.progressHeight; } this.topBorder = border; } removeTopBorder() { if (this.topBorder !== undefined) { this.topBorder = undefined; this.progressHeight = Math.min(Math.max(this.progressHeight - 1, this.progressBuffer.length), this.height); this.consoleHeight = this.height - this.progressHeight; } } setBottomBorder(border) { if (this.bottomBorder === undefined) { this.progressHeight = Math.min(this.height, this.progressHeight + 1); this.consoleHeight = this.height - this.progressHeight; } this.bottomBorder = border; } removeBottomBorder() { if (this.bottomBorder !== undefined) { this.bottomBorder = undefined; this.progressHeight = Math.min(Math.max(this.progressHeight - 1, this.progressBuffer.length), this.height); this.consoleHeight = this.height - this.progressHeight; } } removeProgressSlot() { if (this.progressHeight === 0 || this.progressBuffer.length === 0) { return; } this.progressBuffer.length--; this.progressHeight = Math.min(this.progressLengthWithBorders(), this.height); this.consoleHeight = this.height - this.progressHeight; } currentHeightMinusBorders() { return this.progressHeight - (this.topBorder === undefined ? 0 : 1) - (this.bottomBorder === undefined ? 0 : 1); } progressLengthWithBorders() { return this.progressBuffer.length + (this.topBorder === undefined ? 0 : 1) + (this.bottomBorder === undefined ? 0 : 1); } dumpProgressBuffer() { var _a; const outString = [ this.topBorder, this.progressBuffer .join('\n'), this.bottomBorder, ].filter((v) => (v !== undefined) || (v !== '')) .join('\n'); (_a = this.stream) === null || _a === void 0 ? void 0 : _a.write('' + CUP(0) + ED(0) + '\x1b[0m' + outString); } getBuffer() { return this.progressBuffer; } } const { green } = chalk; const defaultTransformFn = (s) => s; const CHARS = ['\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589', '\u2588']; const FRAC_CHARS = CHARS.slice(0, CHARS.length - 1); const FULL_CHAR = CHARS[CHARS.length - 1]; const SPACE_FILLING_1 = ['\u2801', '\u2809', '\u2819', '\u281B', '\u281E', '\u2856', '\u28C6', '\u28E4', '\u28E0', '\u28A0', '\u2820']; const SPACE_FILLING_2 = ['\u2804', '\u2844', '\u28C4', '\u28E4', '\u28F0', '\u28B2', '\u2833', '\u281B', '\u280B', '\u2809', '\u2808']; const DEFAULT_BORDER = '\u2500'; class MultiProgressBars { /** * * @param options {@link CtorOptions | See CtorOptions Type} */ constructor(options) { this.tasks = {}; this.longestNameLength = 0; this.t = 0; this.endIdx = 0; // 1 past the last index this.allFinished = false; this.headerSettings = { pattern: DEFAULT_BORDER, left: 4, }; this.footerSettings = { pattern: DEFAULT_BORDER, }; this.cleanup = () => { var _a; (_a = this.logger) === null || _a === void 0 ? void 0 : _a.done(); if (this.intervalID) { clearInterval(this.intervalID); } // Resolve the promise. // Should we reject? this.resolve(); // according to node docs, if there's a handler for SIGINT, default behavior // (exiting) is removed, so we have to add it back ourselves. process.exit(); }; // Initialize const options const { // see https://github.com/kamiyo/multi-progress-bars/issues/7 stream = process.stdout.isTTY ? process.stdout : process.stderr, spinnerFPS = 10, spinnerGenerator = this.hilbertSpinner, anchor = 'top', persist = false, border = false, } = options || {}; // Initialize options that might be overwritten let { progressWidth = 40, numCrawlers = 4, initMessage, header, footer, } = options || {}; // New Virtual Console this.logger = new VirtualConsole({ stream, anchor }); this.persist = persist; this.spinnerFPS = Math.min(spinnerFPS, 60); // Just feels right to limit to 60fps this.spinnerGenerator = spinnerGenerator; this.numCrawlers = numCrawlers; this.progressWidth = progressWidth; this.processSimpleBorder(initMessage, border, anchor); // If constructor was supplied additional header option, process that. // Will override initMessage and border options. if (header !== undefined) { this.setHeader(header); } // If constructor was supplied additional footer option, process that. // Will override initMessage and border options. if (footer !== undefined) { this.setFooter(footer); } // Setup cleanup callback for SIGINT process.on('SIGINT', this.cleanup); // Make new unresolved promise this.promise = new Promise((res, _) => this.resolve = res); } /** Make simple border from supplied option */ processSimpleBorder(initMessage, border, anchor) { // If boolean border option, set either null or DEFAULT_BORDER, otherwise set to supplied border if (typeof border === 'boolean') { if (!border) { this.border = null; } else { this.border = DEFAULT_BORDER; } } else { this.border = border; } if (initMessage === undefined) { // Create default initMessage if necessary initMessage = '$ ' + process.argv.map((arg) => { return path__namespace.parse(arg).name; }).join(' '); } else { // Sorry, header message should only be 1 line initMessage = initMessage.split('\n')[0]; } // Make the border if necessary, or just use initMessage if (this.border) { this.headerSettings = Object.assign(Object.assign({}, this.headerSettings), { pattern: this.border, message: initMessage }); initMessage = this.makeBorder(this.headerSettings); } else { initMessage = clampString(initMessage, this.logger.width); } if (this.border) { this.bottomBorder = clampString(this.border.repeat(Math.ceil(this.logger.width / this.border.length)), this.logger.width); } // Set top border and optional bottom border initMessage && this.logger.setTopBorder(initMessage); (this.bottomBorder !== undefined) && this.logger.setBottomBorder(this.bottomBorder); } /** Make border from message, pattern, left, right. Called internally by setHeader and setFooter * * [message] will be placed within a string surrounded by [pattern] at the specified [left] or [right] * Pass undefined or null to [left] if you want [right] to be used. */ makeBorder(border) { let { message, pattern, left, right, } = border; // Build the border with only the pattern first const base = pattern ? clampString(pattern.repeat(Math.ceil(this.logger.width / stringWidth(pattern))), this.logger.width) : ' '.repeat(this.logger.width); if (message === undefined) { return base; } // Clamp message to logger width - 8, because we don't want a potential superwide message // to take up the entire line. Also, remove tabs and newlines, taking the first. message = clampString(message.split(/[\t\n]/g)[0], this.logger.width - 8); // Position from right if supplied if (right !== undefined) { // Some negative indexing for array.slice right = (right <= 0) ? -base.length : Math.floor(right); return clampString(base.slice(0, -right - stringWidth(message)) + message + base.slice(-right), this.logger.width); } // Position from left left = Math.max(Math.floor(left), 0); return clampString(base.slice(0, left) + message + base.slice(left + stringWidth(message)), this.logger.width); } setHeader(options) { if (options !== undefined) { if (typeof options === 'boolean') { if (!options) { this.logger.removeTopBorder(); } else { this.logger.setTopBorder(this.makeBorder(this.headerSettings)); } } else if (typeof options === 'string') { this.logger.setTopBorder(clampString(options.split('\n')[0], this.logger.width)); } else { this.headerSettings = Object.assign(Object.assign({}, this.headerSettings), options); this.logger.setTopBorder(this.makeBorder(this.headerSettings)); } } } setFooter(options) { if (options !== undefined) { if (typeof options === 'boolean') { if (!options) { this.logger.removeBottomBorder(); } else { this.logger.setBottomBorder(this.makeBorder(this.footerSettings)); } } else if (typeof options === 'string') { this.logger.setBottomBorder(clampString(options.split('\n')[0], this.logger.width)); } else { this.footerSettings = Object.assign(Object.assign({}, this.footerSettings), options); this.logger.setBottomBorder(this.makeBorder(this.footerSettings)); } } } removeHeader() { this.setHeader(false); } removeFooter() { this.setFooter(false); } addTask(name, _a) { var { index } = _a, options = __rest(_a, ["index"]); // Restart promise this.restartPromiseIfNeeded(); // Make sure there are no control characters name = stripAnsi(name); if (this.tasks[name] !== undefined) { // if task exists, update fields // remove undefined kv's Object.keys(options).forEach((key) => options[key] === undefined && delete options[key]); // update all other kv's this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), options), { percentage: 0, done: false }); } else { // otherwise make a new task const { type, barTransformFn = defaultTransformFn, nameTransformFn = defaultTransformFn, percentage = 0, message = '', } = options; // Auto-increment index if needed if (index === undefined) { index = this.endIdx; this.endIdx++; } else if (index >= this.endIdx) { this.endIdx = index + 1; } // Make the new task this.tasks[name] = { type, barTransformFn, nameTransformFn, percentage, message, name, index, done: false, }; } // Calculated longest name to pad other names to. this.longestNameLength = Math.max(this.longestNameLength, stringWidth(name)); // If the added task is an indefinite task, and the animation update has previously stopped, // Restart it. if (options.type === 'indefinite' && !this.intervalID) { this.t = 0; this.intervalID = setInterval(() => this.renderIndefinite(), 1000 / this.spinnerFPS); } // Rerender other tasks so that task names are padded correctly. Object.values(this.tasks).forEach((task) => { if (task.type === 'percentage') { this.writeTask(task); } }); this.logger.refresh(); } // Call this BEFORE you add a new task to the list restartPromiseIfNeeded() { // check if allFinished previously // Reset allFinished and make new promise if we need to restart. if (this.allFinished) { this.allFinished = false; this.promise = new Promise((res) => this.resolve = res); } } isDone(name) { return this.tasks[stripAnsi(name)].done; } removeTask(task, shift = true) { // Do nothing if task doesn't exist if ((typeof task === 'string' && this.tasks[stripAnsi(task)] === undefined) || (typeof task === 'number' && this.getName(task) === undefined)) { return; } const idxToRemove = (typeof task === 'string') ? this.tasks[stripAnsi(task)].index : task; // Write empty line to the given index this.logger.upsertProgress({ index: idxToRemove, data: '', }); // Adjust buffers in virtual console if (shift) { this.logger.removeProgressSlot(); this.endIdx--; } // Remove from list of tasks (typeof task === 'string') ? delete this.tasks[stripAnsi(task)] : delete this.tasks[this.getName(task)]; // Shift up tasks if needed, and also recalculate max task name length for realignment this.longestNameLength = Object.entries(this.tasks).reduce((prev, [taskName, { index }]) => { // What?! Side-effects in reduce?! // Don't worry, we're not functional purists here. // Decrement all indexes after the one to remove. if (shift && index > idxToRemove) { this.tasks[taskName].index--; } return Math.max(prev, stringWidth(taskName)); }, 0); // Rerender other tasks so that task names are padded correctly. Object.values(this.tasks).forEach((t) => { this.writeTask(t); }); this.logger.refresh(); } progressString(task) { const { name, barTransformFn, nameTransformFn, message, percentage, } = task; // scale progress bar to percentage of total width const scaled = percentage * this.progressWidth; // scaledInt gives you the number of full blocks const scaledInt = Math.floor(scaled); // scaledFrac gives you the fraction of a full block const scaledFrac = Math.floor(CHARS.length * (scaled % 1)); const fullChar = FULL_CHAR; const fracChar = (scaledFrac > 0) ? FRAC_CHARS[scaledFrac - 1] : ((scaledInt === this.progressWidth) ? '' : ' '); // combine full blocks with partial block const bar = barTransformFn(fullChar.repeat(scaledInt) + fracChar); // fill the rest of the space until progressWidth const rest = (scaledInt < this.progressWidth - 1) ? ' '.repeat(this.progressWidth - (scaledInt + 1)) : ''; // TODO: make this formattable // Currently, returns the name of the task, padded to the length of the longest name, // the bar, space padding, percentage padded to 3 characters, and the custom message. const percentString = (percentage * 100).toFixed(0).padStart(3); // Compensate for the existence of escape characters in padStart. const stringLengthDifference = name.length - stringWidth(name); const paddedTaskName = nameTransformFn(name.padStart(this.longestNameLength + stringLengthDifference)); return `${paddedTaskName}: ${bar}${rest} ${percentString}% | ${message}`; } indefiniteString(task, spinner) { const { name, barTransformFn, nameTransformFn, message, } = task; const stringLengthDifference = name.length - stringWidth(name); const paddedTaskName = nameTransformFn(name.padStart(this.longestNameLength + stringLengthDifference)); return `${paddedTaskName}: ${barTransformFn(spinner)} ${message}`; } writeTask(task) { this.logger.upsertProgress({ index: task.index, data: this.progressString(task), }); } incrementTask(name, _a = {}) { var { percentage = 0.01 } = _a, options = __rest(_a, ["percentage"]); name = stripAnsi(name); if (this.tasks[name] === undefined) { this.logger.error('Error calling incrementTask(): Task does not exist'); return; } if (this.tasks[name].done) { return; } if (this.tasks[name].percentage + percentage > 1) { this.done(name, options); } else { this.updateTask(name, Object.assign(Object.assign({}, options), { percentage: this.tasks[name].percentage + percentage })); } } updateTask(name, options = {}) { name = stripAnsi(name); if (this.tasks[name] === undefined) { this.logger.error('Error calling updateTask(): Task does not exist'); return; } this.restartPromiseIfNeeded(); const task = this.tasks[name]; // Going over 1(00%) calls done if (options.percentage !== undefined && options.percentage > 1) { this.done(name, options); return; } this.tasks[name] = Object.assign(Object.assign(Object.assign({}, task), options), { done: false }); if (task.type === 'indefinite') { if (!this.intervalID) { this.t = 0; this.intervalID = setInterval(() => this.renderIndefinite(), 1000 / this.spinnerFPS); } return; } this.writeTask(this.tasks[name]); this.logger.refresh(); } done(name, _a = {}) { var { message = green('Finished') } = _a, options = __rest(_a, ["message"]); name = stripAnsi(name); if (this.tasks[name] === undefined) { this.logger.error('Error calling done(): Task does not exist'); return; } this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), { done: true, percentage: 1, message }), options); const task = this.tasks[name]; this.writeTask(task); this.logger.refresh(); // Stop animation if all tasks are done, and resolve the promise. if (Object.values(this.tasks).reduce((prev, curr) => { return prev && curr.done; }, true)) { clearInterval(this.intervalID); this.intervalID = null; this.allFinished = true; this.resolve(); if (!this.persist) { this.logger.done(); } } } restartTask(name, _a) { var { percentage = 0 } = _a, options = __rest(_a, ["percentage"]); name = stripAnsi(name); this.restartPromiseIfNeeded(); if (this.tasks[name] === undefined) { this.logger.error('Error calling restart(): Task does not exist'); return; } this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), options), { percentage, done: false }); if (this.tasks[name].type === 'indefinite' && !this.intervalID) { this.t = 0; this.intervalID = setInterval(() => this.renderIndefinite(), 1000 / this.spinnerFPS); } else if (this.tasks[name].type === 'percentage') { this.tasks[name].percentage = 0; this.writeTask(this.tasks[name]); this.logger.refresh(); } } close() { if (this.intervalID !== null) { clearInterval(this.intervalID); this.intervalID = null; } this.allFinished = true; this.resolve(); this.logger.done(); } // Returns the index of task with supplied name. Returns undefined if name not found. getIndex(taskName) { var _a; return (_a = this.tasks[stripAnsi(taskName)]) === null || _a === void 0 ? void 0 : _a.index; } // Returns the name of the task with given index. Returns undefined if name not found. getName(index) { var _a; return (_a = Object.entries(this.tasks).find(([_, task]) => task.index === index)) === null || _a === void 0 ? void 0 : _a[0]; } // TODO maybe make this static? hilbertSpinner(t, width) { // Each cell takes 8 steps to go through (plus 3 for trailing). const cycle = 8 * Math.floor(width / this.numCrawlers); t = t % cycle; const spinner = Array(width).fill(' ').map((_, idx) => { const adjId = -8 * (idx % Math.floor(width / this.numCrawlers)) + t; const leftOver = -cycle + 8; if (idx % 2 === 0) { if (adjId >= leftOver && adjId < leftOver + 3) { return SPACE_FILLING_1[cycle + adjId]; } if (adjId < 0 || adjId >= SPACE_FILLING_1.length) { return ' '; } return SPACE_FILLING_1[adjId]; } else { if (adjId >= leftOver && adjId < leftOver + 3) { return SPACE_FILLING_2[cycle + adjId]; } if (adjId < 0 || adjId >= SPACE_FILLING_2.length) { return ' '; } return SPACE_FILLING_2[adjId]; } }); return spinner.join(''); } renderIndefinite() { const spinner = this.spinnerGenerator(this.t, this.progressWidth); Object.entries(this.tasks).forEach(([_, task]) => { if (task.type === 'indefinite' && task.done === false) { let progressString = this.indefiniteString(task, spinner); this.logger.upsertProgress({ index: task.index, data: progressString, }); } }); this.logger.refresh(); this.t = this.t + 1; } } exports.MultiProgressBars = MultiProgressBars;