multi-progress-bars
Advanced tools
Comparing version 4.0.0-alpha.0 to 4.0.0
@@ -5,2 +5,33 @@ # Changelog | ||
## [4.0.0](https://github.com/kamiyo/multi-progress-bars/compare/v4.0.0-alpha.0...v4.0.0) (2021-06-23) | ||
### Bug Fixes | ||
* **dep:** Update node requirements ([92d95d5](https://github.com/kamiyo/multi-progress-bars/commit/92d95d5a329b354fdd4965ed6939102b78c22202)) | ||
* **dep:** Pin string-width and strip-ansi version to before they moved to es-modules | ||
* **core:** promise resetting was broken | ||
* **core:** bottom-anchored progress bars now has bottom border as well. | ||
### Features | ||
* **core:** barColorFn renamed to barTransformFn | ||
* **core:** added nameTransformFn property | ||
* **core:** added removeTask | ||
* **core:** added extended border options | ||
* **core:** crawler improvements | ||
* **core:** properly handle vertically overflowed progresses | ||
* **core:** dump entire progress buffer on close() or all-complete if there was overflow | ||
* **docs:** Split readme up into docs, and add updated gifs. | ||
* **docs:** Add LICENSE | ||
* **example:** merged top and bottom examples, and added cli options for running them. | ||
### Refactor | ||
* **core:** rewrite virtual-console to allow for overflow and removing. | ||
* **core:** clean-up constructor code. | ||
* **utils:** I dunno why, but template strings for the escape codes. | ||
## [4.0.0-alpha.0](https://github.com/kamiyo/multi-progress-bars/compare/v3.2.4...v4.0.0-alpha.0) (2021-05-18) | ||
@@ -7,0 +38,0 @@ |
@@ -8,2 +8,3 @@ 'use strict'; | ||
var stringWidth = require('string-width'); | ||
var stripAnsi = require('strip-ansi'); | ||
var util = require('util'); | ||
@@ -14,2 +15,3 @@ | ||
var stringWidth__default = /*#__PURE__*/_interopDefaultLegacy(stringWidth); | ||
var stripAnsi__default = /*#__PURE__*/_interopDefaultLegacy(stripAnsi); | ||
@@ -45,3 +47,2 @@ /*! ***************************************************************************** | ||
const CSI = ESC + '['; | ||
const RESET = CSI + '0m'; | ||
const numberTo1StringHelper = (number) => (number !== undefined) ? (number + 1).toFixed(0) : ''; | ||
@@ -53,7 +54,3 @@ /** CUrsor Position | ||
*/ | ||
const CUP = (row, column) => CSI | ||
+ numberTo1StringHelper(row) | ||
+ ';' | ||
+ numberTo1StringHelper(column) | ||
+ 'H'; | ||
const CUP = (row, column) => `${CSI}${numberTo1StringHelper(row)};${numberTo1StringHelper(column)}H`; | ||
var EL_MODE; | ||
@@ -65,4 +62,8 @@ (function (EL_MODE) { | ||
})(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'; | ||
return `${CSI}${mode.toString()}K`; | ||
}; | ||
@@ -76,6 +77,9 @@ var ED_MODE; | ||
})(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'; | ||
return `${CSI}${mode.toString()}J`; | ||
}; | ||
// Always puts a reset ANSI escape code, just in case it was stripped. | ||
// Anyways, probably don't want any styling codes to linger past one line. | ||
@@ -88,3 +92,3 @@ const clampString = (message, width) => { | ||
} | ||
return message + RESET; | ||
return message; | ||
}; | ||
@@ -111,2 +115,3 @@ // Split by newlines, and then split the resulting lines if they run longer than width. | ||
constructor(options) { | ||
var _a; | ||
this.originalConsole = console; | ||
@@ -119,2 +124,7 @@ this.stream = options.stream; | ||
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) { | ||
@@ -126,3 +136,10 @@ this.log = console.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() { | ||
@@ -141,66 +158,5 @@ if (!this.originalConsole) { | ||
done() { | ||
throw new Error('Must Implement in Derived Class!'); | ||
} | ||
refresh() { | ||
throw new Error('Must Implement in Derived Class'); | ||
} | ||
log(..._) { | ||
throw new Error('Must Implement in Derived Class'); | ||
} | ||
upsertProgress(_) { | ||
throw new Error('Must Implement in Dervied Class'); | ||
} | ||
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); | ||
} | ||
setTopBorder(border) { | ||
this.topBorder = border; | ||
this.progressHeight += 1; | ||
} | ||
setBottomBorder(border) { | ||
this.bottomBorder = border; | ||
this.progressHeight += 1; | ||
} | ||
currentHeightMinusBorders() { | ||
return this.progressHeight - (this.topBorder === undefined ? 0 : 1) - (this.bottomBorder === undefined ? 0 : 1); | ||
} | ||
dumpBuffer() { | ||
var _a; | ||
const outString = '' | ||
+ CUP(0) | ||
+ ED(0) | ||
+ '\x1b[0m' | ||
+ ((this.topBorder === undefined) ? '' : (this.topBorder + '\n')) | ||
+ this.progressBuffer | ||
.join('\n') | ||
+ ((this.bottomBorder === undefined) ? '' : ('\n' + this.bottomBorder)); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(outString); | ||
} | ||
getBuffer() { | ||
return this.progressBuffer; | ||
} | ||
} | ||
class VirtualConsoleTop extends VirtualConsole { | ||
constructor(options) { | ||
var _a; | ||
super(options); | ||
this.consoleBuffer = []; | ||
this.consoleHeight = this.height; | ||
this.init(); | ||
(_a = this.refresh) === null || _a === void 0 ? void 0 : _a.call(this); | ||
} | ||
setTopBorder(border) { | ||
super.setTopBorder(border); | ||
this.consoleHeight -= 1; | ||
} | ||
setBottomBorder(border) { | ||
super.setBottomBorder(border); | ||
this.consoleHeight -= 1; | ||
} | ||
done() { | ||
var _a; | ||
if (this.progressBuffer.length > this.height) { | ||
this.dumpBuffer(); | ||
this.dumpProgressBuffer(); | ||
} | ||
@@ -213,23 +169,13 @@ else { | ||
} | ||
/** 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); | ||
/* 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)); | ||
} | ||
getOutString(bufferStartIndex, topLines) { | ||
getOutStringTop(bufferStartIndex, topLines) { | ||
return [ | ||
@@ -247,3 +193,6 @@ topLines.map((val) => val + EL(EL_MODE.TO_END)).join('\n'), | ||
: (this.bottomBorder + EL(EL_MODE.TO_END))), | ||
this.consoleBuffer.map((val) => val + EL(EL_MODE.TO_END)).join('\n') // Logs | ||
(this.consoleBuffer.length) ? | ||
this.consoleBuffer.map((val) => val + EL(EL_MODE.TO_END)).join('\n') | ||
+ ED(ED_MODE.TO_END) | ||
: null, // Logs | ||
].filter((v) => { | ||
@@ -253,13 +202,26 @@ return (v !== undefined) && (v !== '') && (v !== null); // Remove falsey/empty values | ||
} | ||
/* 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) : []; | ||
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)); | ||
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'); | ||
} | ||
log(...data) { | ||
var _a; | ||
// Format incoming strings and split into lines and clamp. | ||
if (data.length !== 0) { | ||
@@ -270,40 +232,4 @@ const writeString = util.format.apply(null, data); | ||
} | ||
// If the console buffer is higher than console height, remove the top, and print them first. | ||
const topLines = (this.consoleBuffer.length > this.consoleHeight) ? | ||
this.consoleBuffer.splice(0, this.consoleBuffer.length - this.consoleHeight) | ||
: []; | ||
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)); | ||
this.refresh(); | ||
} | ||
/** STUB | ||
* | ||
*/ | ||
removeProgressSlot() { | ||
this.progressHeight = Math.max(0, this.progressHeight - 1); | ||
// this.consoleHeight = Math.min(this.height, this.consoleHeight + 1); | ||
// KEEP DOING | ||
} | ||
} | ||
class VirtualConsoleBottom extends VirtualConsole { | ||
constructor(options) { | ||
super(options); | ||
this.init(); | ||
} | ||
init() { | ||
var _a, _b; | ||
super.init(); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(CUP(this.height - 1)); | ||
(_b = this.refresh) === null || _b === void 0 ? void 0 : _b.call(this); | ||
} | ||
done() { | ||
var _a; | ||
if (this.progressBuffer.length > this.height) { | ||
this.dumpBuffer(); | ||
} | ||
else { | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(CUP(this.height, this.width) + '\x1b[0m\n'); | ||
} | ||
console = this.originalConsole; | ||
this.originalConsole = null; | ||
} | ||
/** Add or Update Progress Entry | ||
@@ -325,70 +251,80 @@ * | ||
} | ||
// Extend the progress bars section | ||
// 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); | ||
} | ||
/* Prints out the buffers as they are */ | ||
refresh() { | ||
var _a; | ||
const bufferStartIndex = Math.max(this.progressBuffer.length - this.currentHeightMinusBorders(), 0); | ||
const firstProgressLine = this.height - this.progressHeight; | ||
const outString = [ | ||
((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'); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(outString + CUP(firstProgressLine)); | ||
setTopBorder(border) { | ||
if (this.topBorder === undefined) { | ||
this.progressHeight = Math.min(this.height, this.progressHeight + 1); | ||
this.consoleHeight = this.height - this.progressHeight; | ||
} | ||
this.topBorder = border; | ||
} | ||
log(...data) { | ||
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 -= 1; | ||
this.consoleHeight += 1; | ||
} | ||
} | ||
removeProgressSlot() { | ||
if (this.progressHeight === 0 || this.progressBuffer.length === 0) { | ||
return; | ||
} | ||
this.progressBuffer.length--; | ||
this.progressHeight = Math.min(Math.max(this.progressHeight - 1, this.progressBuffer.length), this.height); | ||
this.consoleHeight = this.height - this.progressHeight; | ||
} | ||
currentHeightMinusBorders() { | ||
return this.progressHeight - | ||
(this.topBorder === undefined ? 0 : 1) - | ||
(this.bottomBorder === undefined ? 0 : 1); | ||
} | ||
dumpProgressBuffer() { | ||
var _a; | ||
let clampedLines = []; | ||
if (data.length !== 0) { | ||
const writeString = util.format.apply(null, data); | ||
// Split by newlines, and then split the resulting lines if they run longer than width. | ||
clampedLines = splitLinesAndClamp(writeString, this.width); | ||
} | ||
const firstProgressLine = this.height - this.progressHeight; | ||
const bufferStartIndex = Math.max(this.progressBuffer.length - this.currentHeightMinusBorders(), 0); | ||
const outString = [ | ||
clampedLines.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 or [] | ||
.slice(bufferStartIndex) | ||
.map((val) => val + EL(EL_MODE.TO_END)) | ||
this.topBorder, | ||
this.progressBuffer | ||
.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'); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(outString + CUP(firstProgressLine)); | ||
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 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 See CtorOptions type | ||
* @param options {@link CtorOptions | See CtorOptions Type} | ||
*/ | ||
constructor(options) { | ||
this.tasks = {}; | ||
this.CHARS = ['\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589', '\u2588']; | ||
this.SPACE_FILLING_1 = ['\u2801', '\u2809', '\u2819', '\u281B', '\u281E', '\u2856', '\u28C6', '\u28E4', | ||
'\u28E0', '\u28A0', '\u2820']; | ||
this.SPACE_FILLING_2 = ['\u2804', '\u2844', '\u28C4', '\u28E4', '\u28F0', '\u28B2', '\u2833', '\u281B', '\u280B', | ||
'\u2809', '\u2808']; | ||
this.FRAC_CHARS = this.CHARS.slice(0, this.CHARS.length - 1); | ||
this.FULL_CHAR = this.CHARS[this.CHARS.length - 1]; | ||
this.longestNameLength = 0; | ||
@@ -398,2 +334,9 @@ this.t = 0; | ||
this.allFinished = false; | ||
this.headerSettings = { | ||
pattern: DEFAULT_BORDER, | ||
left: 4, | ||
}; | ||
this.footerSettings = { | ||
pattern: DEFAULT_BORDER, | ||
}; | ||
this.cleanup = () => { | ||
@@ -405,2 +348,5 @@ var _a; | ||
} | ||
// Resolve the promise. | ||
// Should we reject? | ||
this.resolve(); | ||
// according to node docs, if there's a handler for SIGINT, default behavior | ||
@@ -410,30 +356,47 @@ // (exiting) is removed, so we have to add it back ourselves. | ||
}; | ||
// 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 = 'bottom', persist = false, border = false, } = options || {}; | ||
let { progressWidth = 40, numCrawlers = 4, initMessage, } = options || {}; | ||
this.logger = (anchor === 'top') ? | ||
new VirtualConsoleTop({ stream }) | ||
: new VirtualConsoleBottom({ stream }); | ||
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); | ||
this.spinnerFPS = Math.min(spinnerFPS, 60); // Just feels right to limit to 60fps | ||
this.spinnerGenerator = spinnerGenerator; | ||
this.border = (typeof border === 'boolean') | ||
? (!border) | ||
? null : '\u2500' | ||
: border; | ||
if (progressWidth % 2 !== 0) { | ||
progressWidth += 1; | ||
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 (progressWidth % numCrawlers !== 0) { | ||
for (let i = numCrawlers - 1; i > 0; i++) { | ||
if (progressWidth % i === 0) { | ||
numCrawlers = i; | ||
break; | ||
} | ||
// 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; | ||
} | ||
} | ||
this.numCrawlers = numCrawlers; | ||
this.progressWidth = progressWidth; | ||
else { | ||
this.border = border; | ||
} | ||
if (initMessage === undefined) { | ||
// Create default initMessage if necessary | ||
initMessage = '$ ' + process.argv.map((arg) => { | ||
@@ -444,13 +407,9 @@ return path.parse(arg).name; | ||
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) { | ||
initMessage = clampString(initMessage, this.logger.width - 10); | ||
const startRepeat = (this.border.length > 4) ? 1 : Math.floor(4 / this.border.length); | ||
initMessage = this.border.repeat(startRepeat) + ' ' + initMessage + ' '; | ||
const currentCount = stringWidth__default['default'](initMessage); | ||
const remaining = this.logger.width - currentCount; | ||
const endRepeat = Math.ceil(remaining / this.border.length); | ||
initMessage += this.border.repeat(endRepeat); | ||
initMessage = clampString(initMessage, this.logger.width); | ||
this.headerSettings = Object.assign(Object.assign({}, this.headerSettings), { pattern: this.border, message: initMessage }); | ||
initMessage = this.makeBorder(this.headerSettings); | ||
} | ||
@@ -460,26 +419,96 @@ else { | ||
} | ||
if (this.border && anchor === 'top') { | ||
if (this.border) { | ||
this.bottomBorder = | ||
clampString(this.border.repeat(Math.ceil(this.logger.width / this.border.length)), this.logger.width); | ||
} | ||
this.init(initMessage); | ||
} | ||
init(message) { | ||
// setup cleanup | ||
process.on('SIGINT', this.cleanup); | ||
message && this.logger.setTopBorder(message); | ||
// Set top border and optional bottom border | ||
initMessage && this.logger.setTopBorder(initMessage); | ||
(this.bottomBorder !== undefined) && this.logger.setBottomBorder(this.bottomBorder); | ||
this.promise = new Promise((res, _) => this.resolve = res); | ||
} | ||
/** 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 = clampString(pattern.repeat(Math.ceil(this.logger.width / stringWidth__default['default'](pattern))), this.logger.width); | ||
if (message === undefined) { | ||
return base; | ||
} | ||
// Clamp message - 8, because we don't want a potential superwide message | ||
// to take up the entire line. | ||
message = clampString(message, 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__default['default'](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__default['default'](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(); | ||
// if task exists, update fields | ||
// Make sure there are no control characters | ||
name = stripAnsi__default['default'](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]); | ||
this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), options), { percentage: 0, name, done: false }); | ||
// 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, barColorFn = defaultTransformFn, percentage = 0, message = '', } = options; | ||
const { type, barTransformFn = defaultTransformFn, nameTransformFn = defaultTransformFn, percentage = 0, message = '', } = options; | ||
// Auto-increment index if needed | ||
if (index === undefined) { | ||
@@ -492,5 +521,7 @@ index = this.endIdx; | ||
} | ||
// Make the new task | ||
this.tasks[name] = { | ||
type, | ||
barColorFn, | ||
barTransformFn, | ||
nameTransformFn, | ||
percentage, | ||
@@ -503,3 +534,5 @@ message, | ||
} | ||
// If the added task is an indefinite task, and the animation update has previous stopped, | ||
// Calculated longest name to pad other names to. | ||
this.longestNameLength = Math.max(this.longestNameLength, stringWidth__default['default'](name)); | ||
// If the added task is an indefinite task, and the animation update has previously stopped, | ||
// Restart it. | ||
@@ -510,10 +543,7 @@ if (options.type === 'indefinite' && !this.intervalID) { | ||
} | ||
else if (options.type === 'percentage') { | ||
this.writeTask(this.tasks[name]); | ||
} | ||
// Calculated longest name to pad other names to. | ||
this.longestNameLength = Math.max(this.longestNameLength, stringWidth__default['default'](name)); | ||
// Rerender other tasks so that task names are padded correctly. | ||
Object.values(this.tasks).forEach((task) => { | ||
this.writeTask(task); | ||
if (task.type === 'percentage') { | ||
this.writeTask(task); | ||
} | ||
}); | ||
@@ -525,2 +555,3 @@ this.logger.refresh(); | ||
// check if allFinished previously | ||
// Reset allFinished and make new promise if we need to restart. | ||
if (this.allFinished) { | ||
@@ -532,26 +563,41 @@ this.allFinished = false; | ||
isDone(name) { | ||
return this.tasks[name].done; | ||
return this.tasks[stripAnsi__default['default'](name)].done; | ||
} | ||
// public removeTask(name: string) { | ||
// const idxToRemove = this.tasks[name].index; | ||
// delete this.tasks[name]; | ||
// 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 (index > idxToRemove) { | ||
// this.tasks[taskName].index--; | ||
// } | ||
// return Math.max(prev, stringWidth(taskName)); | ||
// }, 0); | ||
// // Rerender previously finished tasks so that the task names are padded correctly. | ||
// // Do this by calling done() again. | ||
// Object.entries(this.tasks).forEach(([name, { done, message }]) => { | ||
// if (done) { | ||
// this.done(name, { message }); | ||
// } | ||
// }); | ||
// } | ||
removeTask(task, shift = true) { | ||
// Do nothing if task doesn't exist | ||
if ((typeof task === 'string' && this.tasks[stripAnsi__default['default'](task)] === undefined) || | ||
(typeof task === 'number' && this.getName(task) === undefined)) { | ||
return; | ||
} | ||
const idxToRemove = (typeof task === 'string') ? this.tasks[stripAnsi__default['default'](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__default['default'](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__default['default'](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, barColorFn, message, percentage, } = task; | ||
const { name, barTransformFn, nameTransformFn, message, percentage, } = task; | ||
// scale progress bar to percentage of total width | ||
@@ -562,6 +608,6 @@ const scaled = percentage * this.progressWidth; | ||
// scaledFrac gives you the fraction of a full block | ||
const scaledFrac = Math.floor(this.CHARS.length * (scaled % 1)); | ||
const fullChar = this.FULL_CHAR; | ||
const scaledFrac = Math.floor(CHARS.length * (scaled % 1)); | ||
const fullChar = FULL_CHAR; | ||
const fracChar = (scaledFrac > 0) | ||
? this.FRAC_CHARS[scaledFrac - 1] | ||
? FRAC_CHARS[scaledFrac - 1] | ||
: ((scaledInt === this.progressWidth) | ||
@@ -571,3 +617,3 @@ ? '' | ||
// combine full blocks with partial block | ||
const bar = barColorFn(fullChar.repeat(scaledInt) + fracChar); | ||
const bar = barTransformFn(fullChar.repeat(scaledInt) + fracChar); | ||
// fill the rest of the space until progressWidth | ||
@@ -580,14 +626,13 @@ const rest = (scaledInt < this.progressWidth - 1) | ||
// the bar, space padding, percentage padded to 3 characters, and the custom message. | ||
return name.padStart(this.longestNameLength) | ||
+ ': ' | ||
+ bar | ||
+ rest | ||
+ ' ' | ||
+ (percentage * 100).toFixed(0).padStart(3) | ||
+ '% | ' | ||
+ message; | ||
const percentString = (percentage * 100).toFixed(0).padStart(3); | ||
// Compensate for the existence of escape characters in padStart. | ||
const stringLengthDifference = name.length - stringWidth__default['default'](name); | ||
const paddedTaskName = nameTransformFn(name.padStart(this.longestNameLength + stringLengthDifference)); | ||
return `${paddedTaskName}: ${bar}${rest} ${percentString}% | ${message}`; | ||
} | ||
indefiniteString(task, spinner) { | ||
const { name, barColorFn, message, } = task; | ||
return name.padStart(this.longestNameLength) + ': ' + barColorFn(spinner) + ' ' + message; | ||
const { name, barTransformFn, nameTransformFn, message, } = task; | ||
const stringLengthDifference = name.length - stringWidth__default['default'](name); | ||
const paddedTaskName = nameTransformFn(name.padStart(this.longestNameLength + stringLengthDifference)); | ||
return `${paddedTaskName}: ${barTransformFn(spinner)} ${message}`; | ||
} | ||
@@ -602,4 +647,7 @@ writeTask(task) { | ||
var { percentage = 0.01 } = _a, options = __rest(_a, ["percentage"]); | ||
if (this.tasks[name] === undefined) | ||
throw new ReferenceError('Task does not exist.'); | ||
name = stripAnsi__default['default'](name); | ||
if (this.tasks[name] === undefined) { | ||
this.logger.error('Error calling incrementTask(): Task does not exist'); | ||
return; | ||
} | ||
if (this.tasks[name].done) { | ||
@@ -616,4 +664,6 @@ return; | ||
updateTask(name, options = {}) { | ||
name = stripAnsi__default['default'](name); | ||
if (this.tasks[name] === undefined) { | ||
throw new ReferenceError('Task does not exist.'); | ||
this.logger.error('Error calling updateTask(): Task does not exist'); | ||
return; | ||
} | ||
@@ -640,4 +690,7 @@ this.restartPromiseIfNeeded(); | ||
var { message = chalk.green('Finished') } = _a, options = __rest(_a, ["message"]); | ||
if (this.tasks[name] === undefined) | ||
throw new ReferenceError('Task does not exist.'); | ||
name = stripAnsi__default['default'](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); | ||
@@ -660,8 +713,11 @@ const task = this.tasks[name]; | ||
} | ||
restart(name, options) { | ||
restartTask(name, _a) { | ||
var { percentage = 0 } = _a, options = __rest(_a, ["percentage"]); | ||
name = stripAnsi__default['default'](name); | ||
this.restartPromiseIfNeeded(); | ||
if (this.tasks[name] === undefined) { | ||
throw new ReferenceError('Task does not exist.'); | ||
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: 0, done: false }); | ||
this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), options), { percentage, done: false }); | ||
if (this.tasks[name].type === 'indefinite' && !this.intervalID) { | ||
@@ -678,4 +734,9 @@ this.t = 0; | ||
close() { | ||
var _a; | ||
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.done(); | ||
if (this.intervalID !== null) { | ||
clearInterval(this.intervalID); | ||
this.intervalID = null; | ||
} | ||
this.allFinished = true; | ||
this.resolve(); | ||
this.logger.done(); | ||
} | ||
@@ -685,3 +746,3 @@ // Returns the index of task with supplied name. Returns undefined if name not found. | ||
var _a; | ||
return (_a = this.tasks[taskName]) === null || _a === void 0 ? void 0 : _a.index; | ||
return (_a = this.tasks[stripAnsi__default['default'](taskName)]) === null || _a === void 0 ? void 0 : _a.index; | ||
} | ||
@@ -696,22 +757,24 @@ // Returns the name of the task with given index. Returns undefined if name not found. | ||
// Each cell takes 8 steps to go through (plus 3 for trailing). | ||
const cycle = 8 * width / this.numCrawlers; | ||
const cycle = 8 * Math.floor(width / this.numCrawlers); | ||
t = t % cycle; | ||
const spinner = Array(width).fill(' ').map((_, idx) => { | ||
const adjId = -8 * (idx % (width / this.numCrawlers)) + t; | ||
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 this.SPACE_FILLING_1[cycle + adjId]; | ||
return SPACE_FILLING_1[cycle + adjId]; | ||
} | ||
if (adjId < 0 || adjId >= this.SPACE_FILLING_1.length) | ||
if (adjId < 0 || adjId >= SPACE_FILLING_1.length) { | ||
return ' '; | ||
return this.SPACE_FILLING_1[adjId]; | ||
} | ||
return SPACE_FILLING_1[adjId]; | ||
} | ||
else { | ||
if (adjId >= leftOver && adjId < leftOver + 3) { | ||
return this.SPACE_FILLING_2[cycle + adjId]; | ||
return SPACE_FILLING_2[cycle + adjId]; | ||
} | ||
if (adjId < 0 || adjId >= this.SPACE_FILLING_2.length) | ||
if (adjId < 0 || adjId >= SPACE_FILLING_2.length) { | ||
return ' '; | ||
return this.SPACE_FILLING_2[adjId]; | ||
} | ||
return SPACE_FILLING_2[adjId]; | ||
} | ||
@@ -718,0 +781,0 @@ }); |
@@ -5,2 +5,3 @@ /// <reference types="node" /> | ||
declare type TransformFn = (bar: string) => string; | ||
declare type Anchor = 'top' | 'bottom'; | ||
export interface Task { | ||
@@ -11,3 +12,4 @@ name: string; | ||
message: string; | ||
barColorFn: TransformFn; | ||
barTransformFn: TransformFn; | ||
nameTransformFn: TransformFn; | ||
percentage?: number; | ||
@@ -20,6 +22,7 @@ done: boolean; | ||
message?: string; | ||
barColorFn?: TransformFn; | ||
nameTransformFn?: TransformFn; | ||
barTransformFn?: TransformFn; | ||
percentage?: number; | ||
} | ||
export declare type UpdateOptions = Partial<Pick<Task, 'message' | 'barColorFn' | 'percentage'>>; | ||
export declare type UpdateOptions = Partial<Pick<Task, 'message' | 'barTransformFn' | 'percentage' | 'nameTransformFn'>>; | ||
export interface Tasks { | ||
@@ -38,2 +41,11 @@ [key: string]: Task; | ||
/** | ||
* Options for setting the border, all optional | ||
*/ | ||
export interface Border { | ||
message?: string; | ||
pattern?: string; | ||
left?: number; | ||
right?: number; | ||
} | ||
/** | ||
* Constructor Options | ||
@@ -45,3 +57,8 @@ * @param stream A node TTY stream | ||
* @param spinnerGenerator See SpinnerGenerator type | ||
* @param initMessage Message to display above the bars | ||
* @param initMessage Message to display in the header | ||
* @param border boolean or a string for the border design | ||
* @param anchor 'top' or 'bottom', default 'bottom' | ||
* @param persist Keep console override even if all progress bars are 100% | ||
* @param header Optional: Border object, string, or boolean. Will override initMessage and border if provided | ||
* @param footer Optional: Border object, string, or boolean. | ||
*/ | ||
@@ -55,5 +72,7 @@ export interface CtorOptions { | ||
initMessage: string; | ||
anchor: 'top' | 'bottom'; | ||
anchor: Anchor; | ||
persist: boolean; | ||
border: boolean | string; | ||
header: Border | string | boolean; | ||
footer: Border | string | boolean; | ||
} | ||
@@ -64,7 +83,2 @@ export declare class MultiProgressBars { | ||
private progressWidth; | ||
private CHARS; | ||
private SPACE_FILLING_1; | ||
private SPACE_FILLING_2; | ||
private FRAC_CHARS; | ||
private FULL_CHAR; | ||
private persist; | ||
@@ -82,13 +96,27 @@ private intervalID; | ||
private allFinished; | ||
private headerSettings; | ||
private footerSettings; | ||
promise: Promise<void>; | ||
/** | ||
* | ||
* @param options See CtorOptions type | ||
* @param options {@link CtorOptions | See CtorOptions Type} | ||
*/ | ||
constructor(options?: Partial<CtorOptions>); | ||
/** Make simple border from supplied option */ | ||
private processSimpleBorder; | ||
/** 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. | ||
*/ | ||
private makeBorder; | ||
cleanup: () => never; | ||
private init; | ||
setHeader(options: Border | string | boolean): void; | ||
setFooter(options: Border | string | boolean): void; | ||
removeHeader(): void; | ||
removeFooter(): void; | ||
addTask(name: string, { index, ...options }: AddOptions): void; | ||
private restartPromiseIfNeeded; | ||
isDone(name: string): boolean; | ||
removeTask(task: string | number, shift?: boolean): void; | ||
private progressString; | ||
@@ -99,4 +127,4 @@ private indefiniteString; | ||
updateTask(name: string, options?: UpdateOptions): void; | ||
done(name: string, { message, ...options }?: Pick<UpdateOptions, 'message' | 'barColorFn'>): void; | ||
restart(name: string, options: Pick<UpdateOptions, 'message' | 'barColorFn'>): void; | ||
done(name: string, { message, ...options }?: Pick<UpdateOptions, 'message' | 'barTransformFn' | 'nameTransformFn'>): void; | ||
restartTask(name: string, { percentage, ...options }: Pick<UpdateOptions, 'percentage' | 'message' | 'barTransformFn' | 'nameTransformFn'>): void; | ||
close(): void; | ||
@@ -103,0 +131,0 @@ getIndex(taskName: string): number; |
import { green } from 'chalk'; | ||
import { parse } from 'path'; | ||
import stringWidth from 'string-width'; | ||
import stripAnsi from 'strip-ansi'; | ||
import { format } from 'util'; | ||
@@ -35,3 +36,2 @@ | ||
const CSI = ESC + '['; | ||
const RESET = CSI + '0m'; | ||
const numberTo1StringHelper = (number) => (number !== undefined) ? (number + 1).toFixed(0) : ''; | ||
@@ -43,7 +43,3 @@ /** CUrsor Position | ||
*/ | ||
const CUP = (row, column) => CSI | ||
+ numberTo1StringHelper(row) | ||
+ ';' | ||
+ numberTo1StringHelper(column) | ||
+ 'H'; | ||
const CUP = (row, column) => `${CSI}${numberTo1StringHelper(row)};${numberTo1StringHelper(column)}H`; | ||
var EL_MODE; | ||
@@ -55,4 +51,8 @@ (function (EL_MODE) { | ||
})(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'; | ||
return `${CSI}${mode.toString()}K`; | ||
}; | ||
@@ -66,6 +66,9 @@ var ED_MODE; | ||
})(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'; | ||
return `${CSI}${mode.toString()}J`; | ||
}; | ||
// Always puts a reset ANSI escape code, just in case it was stripped. | ||
// Anyways, probably don't want any styling codes to linger past one line. | ||
@@ -78,3 +81,3 @@ const clampString = (message, width) => { | ||
} | ||
return message + RESET; | ||
return message; | ||
}; | ||
@@ -101,2 +104,3 @@ // Split by newlines, and then split the resulting lines if they run longer than width. | ||
constructor(options) { | ||
var _a; | ||
this.originalConsole = console; | ||
@@ -109,2 +113,7 @@ this.stream = options.stream; | ||
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) { | ||
@@ -116,3 +125,10 @@ this.log = console.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() { | ||
@@ -131,66 +147,5 @@ if (!this.originalConsole) { | ||
done() { | ||
throw new Error('Must Implement in Derived Class!'); | ||
} | ||
refresh() { | ||
throw new Error('Must Implement in Derived Class'); | ||
} | ||
log(..._) { | ||
throw new Error('Must Implement in Derived Class'); | ||
} | ||
upsertProgress(_) { | ||
throw new Error('Must Implement in Dervied Class'); | ||
} | ||
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); | ||
} | ||
setTopBorder(border) { | ||
this.topBorder = border; | ||
this.progressHeight += 1; | ||
} | ||
setBottomBorder(border) { | ||
this.bottomBorder = border; | ||
this.progressHeight += 1; | ||
} | ||
currentHeightMinusBorders() { | ||
return this.progressHeight - (this.topBorder === undefined ? 0 : 1) - (this.bottomBorder === undefined ? 0 : 1); | ||
} | ||
dumpBuffer() { | ||
var _a; | ||
const outString = '' | ||
+ CUP(0) | ||
+ ED(0) | ||
+ '\x1b[0m' | ||
+ ((this.topBorder === undefined) ? '' : (this.topBorder + '\n')) | ||
+ this.progressBuffer | ||
.join('\n') | ||
+ ((this.bottomBorder === undefined) ? '' : ('\n' + this.bottomBorder)); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(outString); | ||
} | ||
getBuffer() { | ||
return this.progressBuffer; | ||
} | ||
} | ||
class VirtualConsoleTop extends VirtualConsole { | ||
constructor(options) { | ||
var _a; | ||
super(options); | ||
this.consoleBuffer = []; | ||
this.consoleHeight = this.height; | ||
this.init(); | ||
(_a = this.refresh) === null || _a === void 0 ? void 0 : _a.call(this); | ||
} | ||
setTopBorder(border) { | ||
super.setTopBorder(border); | ||
this.consoleHeight -= 1; | ||
} | ||
setBottomBorder(border) { | ||
super.setBottomBorder(border); | ||
this.consoleHeight -= 1; | ||
} | ||
done() { | ||
var _a; | ||
if (this.progressBuffer.length > this.height) { | ||
this.dumpBuffer(); | ||
this.dumpProgressBuffer(); | ||
} | ||
@@ -203,23 +158,13 @@ else { | ||
} | ||
/** 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); | ||
/* 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)); | ||
} | ||
getOutString(bufferStartIndex, topLines) { | ||
getOutStringTop(bufferStartIndex, topLines) { | ||
return [ | ||
@@ -237,3 +182,6 @@ topLines.map((val) => val + EL(EL_MODE.TO_END)).join('\n'), | ||
: (this.bottomBorder + EL(EL_MODE.TO_END))), | ||
this.consoleBuffer.map((val) => val + EL(EL_MODE.TO_END)).join('\n') // Logs | ||
(this.consoleBuffer.length) ? | ||
this.consoleBuffer.map((val) => val + EL(EL_MODE.TO_END)).join('\n') | ||
+ ED(ED_MODE.TO_END) | ||
: null, // Logs | ||
].filter((v) => { | ||
@@ -243,13 +191,26 @@ return (v !== undefined) && (v !== '') && (v !== null); // Remove falsey/empty values | ||
} | ||
/* 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) : []; | ||
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)); | ||
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'); | ||
} | ||
log(...data) { | ||
var _a; | ||
// Format incoming strings and split into lines and clamp. | ||
if (data.length !== 0) { | ||
@@ -260,40 +221,4 @@ const writeString = format.apply(null, data); | ||
} | ||
// If the console buffer is higher than console height, remove the top, and print them first. | ||
const topLines = (this.consoleBuffer.length > this.consoleHeight) ? | ||
this.consoleBuffer.splice(0, this.consoleBuffer.length - this.consoleHeight) | ||
: []; | ||
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)); | ||
this.refresh(); | ||
} | ||
/** STUB | ||
* | ||
*/ | ||
removeProgressSlot() { | ||
this.progressHeight = Math.max(0, this.progressHeight - 1); | ||
// this.consoleHeight = Math.min(this.height, this.consoleHeight + 1); | ||
// KEEP DOING | ||
} | ||
} | ||
class VirtualConsoleBottom extends VirtualConsole { | ||
constructor(options) { | ||
super(options); | ||
this.init(); | ||
} | ||
init() { | ||
var _a, _b; | ||
super.init(); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(CUP(this.height - 1)); | ||
(_b = this.refresh) === null || _b === void 0 ? void 0 : _b.call(this); | ||
} | ||
done() { | ||
var _a; | ||
if (this.progressBuffer.length > this.height) { | ||
this.dumpBuffer(); | ||
} | ||
else { | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(CUP(this.height, this.width) + '\x1b[0m\n'); | ||
} | ||
console = this.originalConsole; | ||
this.originalConsole = null; | ||
} | ||
/** Add or Update Progress Entry | ||
@@ -315,70 +240,80 @@ * | ||
} | ||
// Extend the progress bars section | ||
// 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); | ||
} | ||
/* Prints out the buffers as they are */ | ||
refresh() { | ||
var _a; | ||
const bufferStartIndex = Math.max(this.progressBuffer.length - this.currentHeightMinusBorders(), 0); | ||
const firstProgressLine = this.height - this.progressHeight; | ||
const outString = [ | ||
((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'); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(outString + CUP(firstProgressLine)); | ||
setTopBorder(border) { | ||
if (this.topBorder === undefined) { | ||
this.progressHeight = Math.min(this.height, this.progressHeight + 1); | ||
this.consoleHeight = this.height - this.progressHeight; | ||
} | ||
this.topBorder = border; | ||
} | ||
log(...data) { | ||
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 -= 1; | ||
this.consoleHeight += 1; | ||
} | ||
} | ||
removeProgressSlot() { | ||
if (this.progressHeight === 0 || this.progressBuffer.length === 0) { | ||
return; | ||
} | ||
this.progressBuffer.length--; | ||
this.progressHeight = Math.min(Math.max(this.progressHeight - 1, this.progressBuffer.length), this.height); | ||
this.consoleHeight = this.height - this.progressHeight; | ||
} | ||
currentHeightMinusBorders() { | ||
return this.progressHeight - | ||
(this.topBorder === undefined ? 0 : 1) - | ||
(this.bottomBorder === undefined ? 0 : 1); | ||
} | ||
dumpProgressBuffer() { | ||
var _a; | ||
let clampedLines = []; | ||
if (data.length !== 0) { | ||
const writeString = format.apply(null, data); | ||
// Split by newlines, and then split the resulting lines if they run longer than width. | ||
clampedLines = splitLinesAndClamp(writeString, this.width); | ||
} | ||
const firstProgressLine = this.height - this.progressHeight; | ||
const bufferStartIndex = Math.max(this.progressBuffer.length - this.currentHeightMinusBorders(), 0); | ||
const outString = [ | ||
clampedLines.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 or [] | ||
.slice(bufferStartIndex) | ||
.map((val) => val + EL(EL_MODE.TO_END)) | ||
this.topBorder, | ||
this.progressBuffer | ||
.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'); | ||
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.write(outString + CUP(firstProgressLine)); | ||
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 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 See CtorOptions type | ||
* @param options {@link CtorOptions | See CtorOptions Type} | ||
*/ | ||
constructor(options) { | ||
this.tasks = {}; | ||
this.CHARS = ['\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589', '\u2588']; | ||
this.SPACE_FILLING_1 = ['\u2801', '\u2809', '\u2819', '\u281B', '\u281E', '\u2856', '\u28C6', '\u28E4', | ||
'\u28E0', '\u28A0', '\u2820']; | ||
this.SPACE_FILLING_2 = ['\u2804', '\u2844', '\u28C4', '\u28E4', '\u28F0', '\u28B2', '\u2833', '\u281B', '\u280B', | ||
'\u2809', '\u2808']; | ||
this.FRAC_CHARS = this.CHARS.slice(0, this.CHARS.length - 1); | ||
this.FULL_CHAR = this.CHARS[this.CHARS.length - 1]; | ||
this.longestNameLength = 0; | ||
@@ -388,2 +323,9 @@ this.t = 0; | ||
this.allFinished = false; | ||
this.headerSettings = { | ||
pattern: DEFAULT_BORDER, | ||
left: 4, | ||
}; | ||
this.footerSettings = { | ||
pattern: DEFAULT_BORDER, | ||
}; | ||
this.cleanup = () => { | ||
@@ -395,2 +337,5 @@ var _a; | ||
} | ||
// Resolve the promise. | ||
// Should we reject? | ||
this.resolve(); | ||
// according to node docs, if there's a handler for SIGINT, default behavior | ||
@@ -400,30 +345,47 @@ // (exiting) is removed, so we have to add it back ourselves. | ||
}; | ||
// 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 = 'bottom', persist = false, border = false, } = options || {}; | ||
let { progressWidth = 40, numCrawlers = 4, initMessage, } = options || {}; | ||
this.logger = (anchor === 'top') ? | ||
new VirtualConsoleTop({ stream }) | ||
: new VirtualConsoleBottom({ stream }); | ||
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); | ||
this.spinnerFPS = Math.min(spinnerFPS, 60); // Just feels right to limit to 60fps | ||
this.spinnerGenerator = spinnerGenerator; | ||
this.border = (typeof border === 'boolean') | ||
? (!border) | ||
? null : '\u2500' | ||
: border; | ||
if (progressWidth % 2 !== 0) { | ||
progressWidth += 1; | ||
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 (progressWidth % numCrawlers !== 0) { | ||
for (let i = numCrawlers - 1; i > 0; i++) { | ||
if (progressWidth % i === 0) { | ||
numCrawlers = i; | ||
break; | ||
} | ||
// 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; | ||
} | ||
} | ||
this.numCrawlers = numCrawlers; | ||
this.progressWidth = progressWidth; | ||
else { | ||
this.border = border; | ||
} | ||
if (initMessage === undefined) { | ||
// Create default initMessage if necessary | ||
initMessage = '$ ' + process.argv.map((arg) => { | ||
@@ -434,13 +396,9 @@ return parse(arg).name; | ||
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) { | ||
initMessage = clampString(initMessage, this.logger.width - 10); | ||
const startRepeat = (this.border.length > 4) ? 1 : Math.floor(4 / this.border.length); | ||
initMessage = this.border.repeat(startRepeat) + ' ' + initMessage + ' '; | ||
const currentCount = stringWidth(initMessage); | ||
const remaining = this.logger.width - currentCount; | ||
const endRepeat = Math.ceil(remaining / this.border.length); | ||
initMessage += this.border.repeat(endRepeat); | ||
initMessage = clampString(initMessage, this.logger.width); | ||
this.headerSettings = Object.assign(Object.assign({}, this.headerSettings), { pattern: this.border, message: initMessage }); | ||
initMessage = this.makeBorder(this.headerSettings); | ||
} | ||
@@ -450,26 +408,96 @@ else { | ||
} | ||
if (this.border && anchor === 'top') { | ||
if (this.border) { | ||
this.bottomBorder = | ||
clampString(this.border.repeat(Math.ceil(this.logger.width / this.border.length)), this.logger.width); | ||
} | ||
this.init(initMessage); | ||
} | ||
init(message) { | ||
// setup cleanup | ||
process.on('SIGINT', this.cleanup); | ||
message && this.logger.setTopBorder(message); | ||
// Set top border and optional bottom border | ||
initMessage && this.logger.setTopBorder(initMessage); | ||
(this.bottomBorder !== undefined) && this.logger.setBottomBorder(this.bottomBorder); | ||
this.promise = new Promise((res, _) => this.resolve = res); | ||
} | ||
/** 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 = clampString(pattern.repeat(Math.ceil(this.logger.width / stringWidth(pattern))), this.logger.width); | ||
if (message === undefined) { | ||
return base; | ||
} | ||
// Clamp message - 8, because we don't want a potential superwide message | ||
// to take up the entire line. | ||
message = clampString(message, 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(); | ||
// if task exists, update fields | ||
// 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]); | ||
this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), options), { percentage: 0, name, done: false }); | ||
// 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, barColorFn = defaultTransformFn, percentage = 0, message = '', } = options; | ||
const { type, barTransformFn = defaultTransformFn, nameTransformFn = defaultTransformFn, percentage = 0, message = '', } = options; | ||
// Auto-increment index if needed | ||
if (index === undefined) { | ||
@@ -482,5 +510,7 @@ index = this.endIdx; | ||
} | ||
// Make the new task | ||
this.tasks[name] = { | ||
type, | ||
barColorFn, | ||
barTransformFn, | ||
nameTransformFn, | ||
percentage, | ||
@@ -493,3 +523,5 @@ message, | ||
} | ||
// If the added task is an indefinite task, and the animation update has previous stopped, | ||
// 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. | ||
@@ -500,10 +532,7 @@ if (options.type === 'indefinite' && !this.intervalID) { | ||
} | ||
else if (options.type === 'percentage') { | ||
this.writeTask(this.tasks[name]); | ||
} | ||
// Calculated longest name to pad other names to. | ||
this.longestNameLength = Math.max(this.longestNameLength, stringWidth(name)); | ||
// Rerender other tasks so that task names are padded correctly. | ||
Object.values(this.tasks).forEach((task) => { | ||
this.writeTask(task); | ||
if (task.type === 'percentage') { | ||
this.writeTask(task); | ||
} | ||
}); | ||
@@ -515,2 +544,3 @@ this.logger.refresh(); | ||
// check if allFinished previously | ||
// Reset allFinished and make new promise if we need to restart. | ||
if (this.allFinished) { | ||
@@ -522,26 +552,41 @@ this.allFinished = false; | ||
isDone(name) { | ||
return this.tasks[name].done; | ||
return this.tasks[stripAnsi(name)].done; | ||
} | ||
// public removeTask(name: string) { | ||
// const idxToRemove = this.tasks[name].index; | ||
// delete this.tasks[name]; | ||
// 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 (index > idxToRemove) { | ||
// this.tasks[taskName].index--; | ||
// } | ||
// return Math.max(prev, stringWidth(taskName)); | ||
// }, 0); | ||
// // Rerender previously finished tasks so that the task names are padded correctly. | ||
// // Do this by calling done() again. | ||
// Object.entries(this.tasks).forEach(([name, { done, message }]) => { | ||
// if (done) { | ||
// this.done(name, { message }); | ||
// } | ||
// }); | ||
// } | ||
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, barColorFn, message, percentage, } = task; | ||
const { name, barTransformFn, nameTransformFn, message, percentage, } = task; | ||
// scale progress bar to percentage of total width | ||
@@ -552,6 +597,6 @@ const scaled = percentage * this.progressWidth; | ||
// scaledFrac gives you the fraction of a full block | ||
const scaledFrac = Math.floor(this.CHARS.length * (scaled % 1)); | ||
const fullChar = this.FULL_CHAR; | ||
const scaledFrac = Math.floor(CHARS.length * (scaled % 1)); | ||
const fullChar = FULL_CHAR; | ||
const fracChar = (scaledFrac > 0) | ||
? this.FRAC_CHARS[scaledFrac - 1] | ||
? FRAC_CHARS[scaledFrac - 1] | ||
: ((scaledInt === this.progressWidth) | ||
@@ -561,3 +606,3 @@ ? '' | ||
// combine full blocks with partial block | ||
const bar = barColorFn(fullChar.repeat(scaledInt) + fracChar); | ||
const bar = barTransformFn(fullChar.repeat(scaledInt) + fracChar); | ||
// fill the rest of the space until progressWidth | ||
@@ -570,14 +615,13 @@ const rest = (scaledInt < this.progressWidth - 1) | ||
// the bar, space padding, percentage padded to 3 characters, and the custom message. | ||
return name.padStart(this.longestNameLength) | ||
+ ': ' | ||
+ bar | ||
+ rest | ||
+ ' ' | ||
+ (percentage * 100).toFixed(0).padStart(3) | ||
+ '% | ' | ||
+ 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, barColorFn, message, } = task; | ||
return name.padStart(this.longestNameLength) + ': ' + barColorFn(spinner) + ' ' + message; | ||
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}`; | ||
} | ||
@@ -592,4 +636,7 @@ writeTask(task) { | ||
var { percentage = 0.01 } = _a, options = __rest(_a, ["percentage"]); | ||
if (this.tasks[name] === undefined) | ||
throw new ReferenceError('Task does not exist.'); | ||
name = stripAnsi(name); | ||
if (this.tasks[name] === undefined) { | ||
this.logger.error('Error calling incrementTask(): Task does not exist'); | ||
return; | ||
} | ||
if (this.tasks[name].done) { | ||
@@ -606,4 +653,6 @@ return; | ||
updateTask(name, options = {}) { | ||
name = stripAnsi(name); | ||
if (this.tasks[name] === undefined) { | ||
throw new ReferenceError('Task does not exist.'); | ||
this.logger.error('Error calling updateTask(): Task does not exist'); | ||
return; | ||
} | ||
@@ -630,4 +679,7 @@ this.restartPromiseIfNeeded(); | ||
var { message = green('Finished') } = _a, options = __rest(_a, ["message"]); | ||
if (this.tasks[name] === undefined) | ||
throw new ReferenceError('Task does not exist.'); | ||
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); | ||
@@ -650,8 +702,11 @@ const task = this.tasks[name]; | ||
} | ||
restart(name, options) { | ||
restartTask(name, _a) { | ||
var { percentage = 0 } = _a, options = __rest(_a, ["percentage"]); | ||
name = stripAnsi(name); | ||
this.restartPromiseIfNeeded(); | ||
if (this.tasks[name] === undefined) { | ||
throw new ReferenceError('Task does not exist.'); | ||
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: 0, done: false }); | ||
this.tasks[name] = Object.assign(Object.assign(Object.assign({}, this.tasks[name]), options), { percentage, done: false }); | ||
if (this.tasks[name].type === 'indefinite' && !this.intervalID) { | ||
@@ -668,4 +723,9 @@ this.t = 0; | ||
close() { | ||
var _a; | ||
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.done(); | ||
if (this.intervalID !== null) { | ||
clearInterval(this.intervalID); | ||
this.intervalID = null; | ||
} | ||
this.allFinished = true; | ||
this.resolve(); | ||
this.logger.done(); | ||
} | ||
@@ -675,3 +735,3 @@ // Returns the index of task with supplied name. Returns undefined if name not found. | ||
var _a; | ||
return (_a = this.tasks[taskName]) === null || _a === void 0 ? void 0 : _a.index; | ||
return (_a = this.tasks[stripAnsi(taskName)]) === null || _a === void 0 ? void 0 : _a.index; | ||
} | ||
@@ -686,22 +746,24 @@ // Returns the name of the task with given index. Returns undefined if name not found. | ||
// Each cell takes 8 steps to go through (plus 3 for trailing). | ||
const cycle = 8 * width / this.numCrawlers; | ||
const cycle = 8 * Math.floor(width / this.numCrawlers); | ||
t = t % cycle; | ||
const spinner = Array(width).fill(' ').map((_, idx) => { | ||
const adjId = -8 * (idx % (width / this.numCrawlers)) + t; | ||
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 this.SPACE_FILLING_1[cycle + adjId]; | ||
return SPACE_FILLING_1[cycle + adjId]; | ||
} | ||
if (adjId < 0 || adjId >= this.SPACE_FILLING_1.length) | ||
if (adjId < 0 || adjId >= SPACE_FILLING_1.length) { | ||
return ' '; | ||
return this.SPACE_FILLING_1[adjId]; | ||
} | ||
return SPACE_FILLING_1[adjId]; | ||
} | ||
else { | ||
if (adjId >= leftOver && adjId < leftOver + 3) { | ||
return this.SPACE_FILLING_2[cycle + adjId]; | ||
return SPACE_FILLING_2[cycle + adjId]; | ||
} | ||
if (adjId < 0 || adjId >= this.SPACE_FILLING_2.length) | ||
if (adjId < 0 || adjId >= SPACE_FILLING_2.length) { | ||
return ' '; | ||
return this.SPACE_FILLING_2[adjId]; | ||
} | ||
return SPACE_FILLING_2[adjId]; | ||
} | ||
@@ -708,0 +770,0 @@ }); |
@@ -44,2 +44,6 @@ export declare const RESET: string; | ||
} | ||
/** Erase Line | ||
* | ||
* @param mode EL_MODE.TO_END, .TO_BEGINNING, or .ENTIRE_LINE | ||
*/ | ||
export declare const EL: (mode?: EL_MODE) => string; | ||
@@ -52,4 +56,8 @@ export declare enum ED_MODE { | ||
} | ||
/** Erase Display | ||
* | ||
* @param mode ED_MORE.TO_END, .TO_BEGINNING, ENTIRE_SCREEN, or .ENTIRE_SCREEN_DELETE_SCROLLBACK | ||
*/ | ||
export declare const ED: (mode?: ED_MODE) => string; | ||
export declare const clampString: (message: string, width: number) => string; | ||
export declare const splitLinesAndClamp: (writeString: string, maxWidth: number) => string[]; |
@@ -5,2 +5,3 @@ /// <reference types="node" /> | ||
stream: WriteStream; | ||
anchor?: 'top' | 'bottom'; | ||
} | ||
@@ -15,12 +16,15 @@ export interface AddProgressOptions { | ||
export declare class VirtualConsole { | ||
protected progressBuffer: string[]; | ||
protected height: number; | ||
private progressBuffer; | ||
private height; | ||
/** Progress section height, will not exceed total terminal height | ||
* As opposed to progressBuffer.length, which is unbounded | ||
*/ | ||
protected progressHeight: number; | ||
protected topBorder: string; | ||
protected bottomBorder: string; | ||
protected originalConsole: Console; | ||
protected stream: WriteStream; | ||
private progressHeight; | ||
private consoleBuffer; | ||
private consoleHeight; | ||
private topBorder; | ||
private bottomBorder; | ||
private originalConsole; | ||
private stream; | ||
private getOutString; | ||
width: number; | ||
@@ -30,2 +34,3 @@ warn: Console['warn']; | ||
constructor(options: VirtualConsoleCtorOptions); | ||
init(): void; | ||
checkConsoleIntercept(): void; | ||
@@ -35,18 +40,5 @@ resize(): void; | ||
refresh(): void; | ||
log(..._: any[]): void; | ||
upsertProgress(_: AddProgressOptions): void; | ||
init(): void; | ||
setTopBorder(border: string): void; | ||
setBottomBorder(border: string): void; | ||
protected currentHeightMinusBorders(): number; | ||
dumpBuffer(): void; | ||
getBuffer(): string[]; | ||
} | ||
export declare class VirtualConsoleTop extends VirtualConsole { | ||
private consoleBuffer; | ||
private consoleHeight; | ||
constructor(options: VirtualConsoleCtorOptions); | ||
setTopBorder(border: string): void; | ||
setBottomBorder(border: string): void; | ||
done(): void; | ||
getOutStringTop(bufferStartIndex: number, topLines: string[]): string; | ||
getOutStringBottom(bufferStartIndex: number, topLines: string[]): string; | ||
log(...data: any[]): void; | ||
/** Add or Update Progress Entry | ||
@@ -59,23 +51,10 @@ * | ||
upsertProgress(options: AddProgressOptions): void; | ||
getOutString(bufferStartIndex: number, topLines: string[]): string; | ||
refresh(): void; | ||
log(...data: any[]): void; | ||
/** STUB | ||
* | ||
*/ | ||
setTopBorder(border: string): void; | ||
removeTopBorder(): void; | ||
setBottomBorder(border: string): void; | ||
removeBottomBorder(): void; | ||
removeProgressSlot(): void; | ||
private currentHeightMinusBorders; | ||
dumpProgressBuffer(): void; | ||
getBuffer(): string[]; | ||
} | ||
export declare class VirtualConsoleBottom extends VirtualConsole { | ||
constructor(options: VirtualConsoleCtorOptions); | ||
init(): void; | ||
done(): void; | ||
/** Add or Update Progress Entry | ||
* | ||
* @param options | ||
* index: number | ||
* data: string | ||
*/ | ||
upsertProgress(options: AddProgressOptions): void; | ||
refresh(): void; | ||
log(...data: any[]): void; | ||
} |
{ | ||
"name": "multi-progress-bars", | ||
"version": "4.0.0-alpha.0", | ||
"version": "4.0.0", | ||
"description": "Multiple progress bars with option for indefinite spinners", | ||
"main": "dist/multi-progress-bars.cjs.js", | ||
"module": "dist/multi-progress-bars.es.js", | ||
"exports": { | ||
".": { | ||
"require": "./dist/multi-progress-bars.cjs.js", | ||
"default": "./dist/multi-progress-bars.es.js" | ||
} | ||
}, | ||
"type": "commonjs", | ||
"types": "dist/index.d.ts", | ||
@@ -17,3 +24,3 @@ "files": [ | ||
"clean": "yarn trash 'dist/**/*' 'examples/*.js'", | ||
"runExample": "yarn build && yarn buildExamples && yarn node examples/exampleBottom.js" | ||
"runExample": "yarn build && yarn buildExamples && yarn node examples/example.js" | ||
}, | ||
@@ -36,4 +43,7 @@ "author": "Sean Chen <kamiyo@gmail.com>", | ||
}, | ||
"engines": { | ||
"node": ">13" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^12.19.0", | ||
"@types/node": ">13.0.0", | ||
"@wessberg/rollup-plugin-ts": "^1.3.8", | ||
@@ -48,5 +58,6 @@ "rollup": "^2.40.0", | ||
"dependencies": { | ||
"chalk": "^4.1.0", | ||
"string-width": "^4.2.1" | ||
"chalk": "^4.1.1", | ||
"string-width": "4.2.2", | ||
"strip-ansi": "6.0.0" | ||
} | ||
} |
313
README.md
# multi-progress-bars | ||
![npm shield](https://img.shields.io/npm/v/multi-progress-bars) | ||
![npm shield](https://img.shields.io/npm/v/multi-progress-bars) ![node shield](https://img.shields.io/npm/dm/multi-progress-bars) | ||
> :warning: v3 technically has **breaking changes** in behavior nuance, but should be backwards-compatible. The changes are the addition of anchor position, border, and persist. If you pass in `true` for persist, you should call `mpb.close()` when you are done, or on `SIGINT` for example. Multi-line init messages are now clamped to the first line. | ||
A node utility written in Typescript for displaying multiple progress bars, and preserving logged output. The progress bars can be anchored to the top or bottom of the terminal, with border options and indefinite progress display. A Promise will resolve when all tasks are done. This utility is enabled by a simple virtual console that intercepts the normal console. See the below demo .gifs. | ||
A node library for displaying multiple progress bars, with an option for displaying indefinite tasks by using a spinner. Works well with gulp.js and/or webpack. This library has two display options: top and bottom anchor. | ||
___ | ||
Anchoring the progress bars to the top will clear the screen non-destructively, and display the bars from the top; any subsequent calls to `console.log` will not affect the fixed progress rows, but overflow will push previous console messages above the fold. This is enabled by a simple virtual console. This way no console messages will be lost. See the below demo .gif. | ||
Anchoring to the top. Notice how the logs are preserved: | ||
![MultiProgressBar top demo](./assets/mpb_top.gif) | ||
Anchoring to the bottom is a bit simpler, but is just as useful. | ||
--- | ||
Anchoring to the bottom: | ||
![MultiProgressBar bottom demo](./assets/mpb_bottom.gif) | ||
--- | ||
Showing off borders customizability and overflow management: | ||
![MultiProgressBar extended demo](./assets/mpb_extended.gif) | ||
## Install | ||
`npm install multi-progress-bars` | ||
or | ||
`npm install multi-progress-bars`<br> | ||
or<br> | ||
`yarn add multi-progress-bars` | ||
## Usage | ||
Each bar is represented internally by a `Task`. First instantiate `MultiProgressBars` with options like `anchor` and `border`: | ||
```node | ||
## Quickstart | ||
``` | ||
import { MultiProgressBars } from 'multi-progress-bars'; | ||
import * as chalk from 'chalk'; | ||
const mpb = new MultiProgressBars(/* options object */); | ||
``` | ||
// Initialize mpb | ||
const mpb = new MultiProgressBars({ | ||
initMessage: ' $ Example Fullstack Build ', | ||
anchor: 'top', | ||
persist: true, | ||
border: true, | ||
}); | ||
Then you can add bars by adding tasks: | ||
```node | ||
const Task1 = 'Task 1' | ||
mpb.addTask(Task1, { type: 'percentage', index: 0 }); | ||
``` | ||
// Add tasks | ||
mpb.addTask('Webpack Backend', { type: 'percentage', barColorFn: chalk.yellow }); | ||
mpb.addTask('Watcher', { type: 'indefinite', message: 'Watching for changes...' }); | ||
Spice up the bars by passing in a string transform function: | ||
```node | ||
import * as chalk from 'chalk'; // or colors.js | ||
// Update tasks | ||
mpb.updateTask('Webpack Backend', { percentage: 0.2 }); | ||
mpb.incrementTask('Webpack Backend', { percentage: 0.1 }); | ||
const ColorBarTask = 'Task Color' | ||
mpb.addTask(Task Color, { type: 'percentage', index: 1, barColorFn: chalk.green }); | ||
``` | ||
// Console logging is overridden and tracked with an interval buffer | ||
console.log('Warning!'); | ||
Create an indefinite spinner by: | ||
```node | ||
const Task2 = 'Task 2' | ||
mpb.addTask(Task2, { type: 'indefinite', index: 2 }); | ||
``` | ||
setTimeout(() => { mpb.done('Webpack Backend', { message: 'Build finished.' })}, 5000); | ||
setTimeout(() => { mpb.done('Watcher', { message: 'Build finished.' })}, 1000); | ||
Update the bars with either incrementTask or updateTask: | ||
```node | ||
mpb.updateTask(Task1, { percentage: 0.3 }); | ||
// or | ||
mpb.incrementTask(Task1); | ||
``` | ||
// Wait for all tasks to finish | ||
await mpb.promise; | ||
console.log('Finish'); | ||
When task is done call done(). A percentage of 1(00%) does not imply done: | ||
```node | ||
mpb.done(Task1); | ||
``` | ||
See the [Documentation](#documentation) for more information. | ||
multi-progress-bars exposes a Promise that will be resolved when all tasks are done: | ||
```node | ||
await mpb.promise; | ||
// Do stuff here when all tasks finish | ||
// Promise will be reset when tasks are restarted or new tasks are added | ||
``` | ||
Check out the example directory for a working example. | ||
## Version Changes | ||
## API | ||
<br> | ||
Note: `percentage` is represented as the decimal form, i.e. 0.23 = 23% | ||
> ## :warning: v3 -> v4 has **breaking changes**.<br> | ||
> **User-Facing Changes**: | ||
>* `barColorFn` is now `barTransformFn`, and a new property `nameTransformFn` has been added to transform task names appearance. | ||
>* Number of progress bars displayed will be truncated to terminal height (accounting for borders) | ||
>* Upon exiting, will dump the full untruncated list of progress bars if the above was true. | ||
>* API added for removing tasks, with option to shift up the tasks or leave a blank line. | ||
>* API added for getting the index of a specific task, or getting the name of a task given the index. | ||
>* Extra border options in constructor as well as API to set or remove headers and footers. | ||
>* Bottom-anchored progress bars now has bottom border if `border` is set to `true` in the constructor. | ||
### `new MultiProgressBar(options)` | ||
<br> | ||
`options` `<object>` (all optional): | ||
* `stream` `<TTY.WriteStream>` Can be `process.stdout` or `process.stderr`. default = `process.stdout` | ||
* `spinnerFPS` `<number>` The FPS to update the spinner. default = `10` | ||
* `progressWidth` `<number>` The width of the progress bars. This will be incremented if odd (it's just easier to deal with even width). default = `40` | ||
* `numCrawlers` `<number>` The number of crawlers for the infinite spinner. Omit if providing on spinner generator fn. This will be decremented until it is a factor of progressWidth. default = `4` | ||
* `spinnerGenerator` `<(t: number, width: number) => string>` A function that takes the current timestamp and total width and returns a string. default = `mpb.hilbertSpinner` | ||
* `initMessage` `<string>` A persistent message to display above the bars. If it is multi-line or longer than the console width, it will take the first line and/or clamp it. default = `'$ ' + process.argv.map((arg) => { return path.parse(arg).name; }).join(' ');` | ||
* `anchor` `<'top' | 'bottom'>` The position of the progress bars. default (to match with previous behavior) = `'top'` | ||
* `border` `<boolean | string>` If set to true, will use U+2500 as the character. If set to false, will not show border. If set to string, will use said string to form borders. Bottom-anchored progress section will only have a top border. Top-anchored section will have both top and bottom borders. default = `false` | ||
* `persist` `<boolean>` When true, mpb will continue to intercept console functions even when all the tasks are completed; you must call `mpb.close()` to get back the original console (or else you might get wonky prompt placement afterwards). If false, once all tasks have completed, mpb will automatically restore the original console. However, if you restart a task, it will re-intercept. Use true if doing something like a webpack watch. default = `false` | ||
> :warning: v2 -> v3 technically has **breaking changes** in behavior nuance, but should be backwards-compatible. The changes are the addition of anchor position, border, and persist. If you pass in `true` for persist, you should call `mpb.close()` when you are done, or on `SIGINT` for example. Multi-line init messages are now clamped to the first line. | ||
### `mpb.addTask(name, options)` | ||
<br> | ||
`name` `<string>` Task name. All subsequent actions on the task will be called with this same name. | ||
## <span name="documentation">Documentation</span> | ||
`options` `<object>`: | ||
* `type` `<'percentage' | 'indefinite'>` required. | ||
* `index` `<number>` optional. default = increment from previous || 0. | ||
* `percentage` `<number>` optional. The starting percentage (0 to 1) if type is `'percentage'`. default = `0` | ||
* `message` `<string>` optional. A message to print to the right of the bar. default = `''` | ||
* `barColorFn` `<(s: string) => string>` optional. A function that transforms the bar. Useful for coloring the bar with `chalk.js` or `colors.js`. default = `(s) => s`; | ||
* [Usage guide](docs/Usage.md) | ||
* [API guide](docs/API.md) | ||
* [Changelog](CHANGELOG.md) | ||
* [Examples](docs/Examples.md) | ||
Not only does this method add a new Task, but if you pass in a name that already exists, it will restart the Task (sets the `percentage` back to `0` and `done` to be `false`). This makes coding reporter logic easier, instead of having to check if the task is done; you can just always call `addTask('Task Name')` at the start of, say, a `watch` function. | ||
### `mpb.incrementTask(name, options)` | ||
`name` `<string>` Task name. | ||
`options` `<object>` (unset properties will not affect change unless a default exists): | ||
* `message` `<string>` optional. A message to print to the right of the bar. | ||
* `percentage` `<number>` optional. The amount to increment by. default = `0.01` | ||
* `barColorFn` `<(s: string) => string>` optional. A function that transforms the bar. | ||
Incrementing a task to above 1(00%) will automatically call `done` on it. | ||
### `mpb.updateTask(name, options)` | ||
`name` `<string>` Task name. | ||
`options` `<object>` (unset properties will not affect change): | ||
* `message` `<string>` optional. A message to print to the right of the bar. | ||
* `percentage` `<number>` optional. The amount to change the percentage to if task type is `'percentage'`. | ||
* `barColorFn` `<(s: string) => string>` optional. A function that transforms the bar. | ||
Calling updateTask with a percentage over 1(00%) will automatically set it to done. Calling updateTask on an task with `done: true` will restart it | ||
## `mpb.getIndex(name)` | ||
`name` `<string>` Task name. | ||
Get the index of the task. If name is not found, returns `undefined`. | ||
## `mpb.getName(index)` | ||
`index` `<number>` Task index. | ||
Get the name of the task with given index. If the index is not found, returns `undefined`. | ||
## `mpb.done(name, options)` | ||
`name` `<string>` Task name. | ||
`options` `<object>` (unset properties will not affect change): | ||
* `message` `<string>` optional. A message to print to the right of the bar. default = `chalk.green('Finished')` | ||
* `barColorFn` `<(s: string) => string>` optional. A function that transforms the bar. | ||
### `mpb.restart(name, options)` | ||
`name` `<string>` Task name. | ||
`options` `<object>` (unset properties will not affect change): | ||
* `message` `<string>` optional. A message to print to the right of the bar. | ||
* `barColorFn` `<(s: string) => string>` optional. A function that transforms the bar. | ||
### `mpb.close()` | ||
This will restore the original console to `console`, and move the cursor to expected position for the next console prompt. If you restart tasks after mpb has closed, it will re-intercept console again. | ||
### `mpb.promise` | ||
`<Promise>` A promise that will be resolved when all tasks are `done`. This allows you to defer rendering of reporters until after, which prevents write race conditions that mess up the bar rendering. The promise will be reset if any tasks are restarted. | ||
## Hilbert Spinner | ||
Included in this library is a cool Hilbert Curve / Space-Filling Curve spinner. It uses the Braille dots to do a little snake that crawls throughout the bar. There is probably a more efficient way to code the crawling, as this uses sort of an 'implicit' approach, instead of an 'explicit', which might require less calculation. | ||
Included in this library is a (imo) cool Hilbert Curve / Space-Filling Curve spinner. It uses the Braille dots to do a little snake that crawls throughout the bar. There is probably a more efficient way to code the crawling, as this uses sort of an 'implicit' approach, instead of an 'explicit', which might require less calculation. | ||
## Using with Gulp and Webpack | ||
## License | ||
You can print status for, say, a typescript compilation like this (this is in javascript, not typescript): | ||
[MIT](LICENSE) | ||
```node | ||
const glob = require('glob'); | ||
const { Transform } = require('stream'); | ||
const ts = require('gulp-typescript'); | ||
const { MultiProgressBars } = require('multi-progress-bars'); | ||
const chalk = require('chalk'); | ||
## Like this utility? Support my hobby! | ||
const mpb = new MultiProgressBars(); | ||
const tsProject = ts.createProject('./tsconfig.json'); | ||
const compileTS = () => { | ||
const count = glob.sync('src/**/*.ts').length; | ||
mpb.addTask('Compile Typescript', { | ||
type: 'percentage', | ||
index: 0, | ||
barColorFn: chalk.blue | ||
}); | ||
let counter = 0; | ||
const forEach = new Transform({ | ||
writeableObjectMode: true, | ||
readableObjectMode: true, | ||
transform(chunk, _, callback) { | ||
counter++; | ||
mpb.updateTask('Compile Typescript', { | ||
percentage: counter / count, | ||
message: counter + '/' + count, | ||
}); | ||
callback(null, chunk); | ||
} | ||
}); | ||
mpb.promise.then(() => { | ||
console.log('All Tasks Finished!'); | ||
}); | ||
return tsProject.src() | ||
.pipe(forEach) | ||
.pipe(tsProject( | ||
ts.reporter.nullReporter() | ||
)) | ||
.js | ||
.pipe(gulp.dest('./build')) | ||
.on('end', () => { | ||
mpb.done('Compile Typescript'); | ||
}); | ||
} | ||
exports.compileTS = compileTS; | ||
``` | ||
Pair that with a `watch`. | ||
You can also add mpb to your Webpack config by passing in the mpb instance to a function that returns the config: | ||
```node | ||
// webpack.dev.config.js | ||
const config = (mpb) => { | ||
return { | ||
// ... other config | ||
plugins: [ | ||
// ... other plugins | ||
new webpack.ProgressPlugin({ | ||
handler: (percentage, message, ...args) => { | ||
const msg = message | ||
? message + | ||
((args.length) ? ': ' + args[0] : '') | ||
: ''; | ||
mpb.updateTask( | ||
'Webpack', | ||
{ | ||
percentage, | ||
message: msg, | ||
} | ||
); | ||
} | ||
}), | ||
], | ||
}; | ||
} | ||
module.exports = config; | ||
// gulpfile.js | ||
const webpack = require('webpack'); | ||
const { MultiProgressBars } = require('multi-progress-bars'); | ||
const mpb = new MultiProgressBars({ persist: true }); | ||
const webpackConfig = require('./webpack.dev.config.js'); | ||
const webpackWatch = (done) => { | ||
mpb.addTask('Webpack', { | ||
type: 'percentage', | ||
index: 0, | ||
}); | ||
const compiler = webpack(webpackConfig(mpb)); | ||
compiler.hooks.beforeRun.tapAsync('Reset Progress', (p) => { | ||
// for restarts | ||
mpb.updateTask('Webpack', { | ||
percentage: 0, | ||
}); | ||
}); | ||
compiler.watch({ ignore: /node_modules/ }, (err, stats) => { | ||
mpb.done('Webpack') | ||
mpb.promise.then(() => { | ||
console.log(stats.toString()); | ||
}); | ||
}); | ||
done(); | ||
}; | ||
exports.watch = webpackWatch; | ||
``` | ||
N.B. Above code not 100% tested. | ||
## TODO | ||
* Decouple hilbertSpinner from the instance. | ||
* Allow custom bar format | ||
* Allow custom progress format | ||
[PayPal donation link](https://paypal.me/seanchenpiano?locale.x=en_US) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
89571
10
1743
0
3
102
+ Addedstrip-ansi@6.0.0
+ Addedstring-width@4.2.2(transitive)
+ Addedstrip-ansi@6.0.0(transitive)
- Removedstring-width@4.2.3(transitive)
- Removedstrip-ansi@6.0.1(transitive)
Updatedchalk@^4.1.1
Updatedstring-width@4.2.2