UNPKG

ascii-bar

Version:

A zero dependency ascii progress bar with spinner, colors and typescript support

399 lines (339 loc) 12.7 kB
export default class AsciiBar { /** * Format of the displayed progressbar */ public formatString = '#percent #bar'; /** * Number of steps to finish progress */ public total = 100; /** * Startdate to calculate elapsed time (in milliseconds) */ public startDate = new Date().getTime(); /** * Which timespan to use for timing calculation - If you are unsure allways use false here! */ public lastUpdateForTiming = false; /** * Width of the progress bar (only the #bar part) */ public width = 20; /** * Symbol for the done progress in the #bar part */ public doneSymbol = ">"; /** * Symbol for the undone progress in the #bar part */ public undoneSymbol = "-"; /** * Wether to print to configured stream or not */ public print = true; /** * A spinner object describing how the spinner looks like * Change this for another spinner */ public spinner = defaultSpinner; /** * The message displayed at the #message placeholder */ public message = ""; /** * wether to call progressbar's stop() function automatically if the progress reaches 100% */ public autoStop = true; /** * wether to hide the terminal's cursor while displaying the progress bar */ public hideCursor = false; private elapsed = 0; private lastUpdate = new Date().getTime(); private timeToFinish = 0; private overallTime = 0; private stream = process.stdout; private spinnerTimeout; private enableSpinner = false; private currentSpinnerSymbol = ""; private current = 0; constructor(options?: string | ProgressbarOptions) { if (options) { //if only a string was provided, use this as formatString if (typeof options == "string") { options = { formatString: options } } //set other options for (const opt in options) { if (this[opt] !== undefined) { this[opt] = options[opt]; } } //set start value if (options.start) { this.current = options.start; } //use simple spinner on windows if (process.platform === 'win32') { this.spinner = simpleSpinner; } //enable spinner if (this.enableSpinner) { this.spinnerTimeout = setTimeout(this.updateSpinner, this.spinner.interval); } } } /** * Creates the progressbar string with all configured settings * @returns a string representating the progressbar */ public renderLine(): string { let plusCount = Math.round(this.current / this.total * this.width); let minusCount = this.width - plusCount; let plusString = ""; let minusString = ""; for (let i = 0; i < plusCount; i++) { plusString += this.doneSymbol; } for (let i = 0; i < minusCount; i++) { minusString += this.undoneSymbol; } let barString = `[${plusString}${minusString}]`; let currentString = String(this.current); while (currentString.length < String(this.total).length) { currentString = "0" + currentString; } let countString = `[${currentString}/${this.total}]`; let percentString = String(Math.round((this.current / this.total) * 100)); while (percentString.length < 3) { percentString = " " + percentString; } percentString += "%"; let overAllString = this.formatTime(this.overallTime); let elapsedString = this.formatTime(this.elapsed); let ttfString = this.formatTime(this.timeToFinish); //Replace macros let line = this.formatString.replace(/#bar/g, barString).replace(/#count/g, countString).replace(/#percent/g, percentString).replace(/#overall/g, overAllString).replace(/#elapsed/g, elapsedString).replace(/#ttf/g, ttfString).replace(/#message/g, this.message).replace(/#spinner/g, this.currentSpinnerSymbol); //Colors :-) line = line.replace(/##default/g, colorCodes.Reset).replace(/##green/g, colorCodes.Green).replace(/##blue/g, colorCodes.Blue).replace(/##red/g, colorCodes.Red).replace(/##yellow/g, colorCodes.Yellow).replace(/##bright/g, colorCodes.Bright).replace(/##dim/g, colorCodes.Dim); line += colorCodes.Reset; //Hide cursor if (this.hideCursor) { line = colorCodes.HideCursor + line; } return line; } /** * Render the progressbar and print it to output stream */ public printLine(): void { if (!this.print) { return; } this.stream.cursorTo(0); this.stream.write(this.renderLine()); this.stream.clearLine(1); } /** * update the progress. This will trigger re-rendering the progressbar * @param current the new absolute progress value * @param message [optional] update the message displayed at the #message placeholder */ public update(current: number, message?: string) { this.current = current; if (message) { this.message = message; } //timePerTick * max = overallTime //timePerTick = elapsed / current //overallTime = (elapsed / current) * max //timeToFinish = overallTime - elapsed let now = new Date().getTime(); //how to calculate time per step let timePerStep = this.lastUpdateForTiming ? (now - this.lastUpdate) : (this.elapsed / this.current); this.elapsed = now - this.startDate; this.overallTime = (timePerStep * this.total); this.timeToFinish = (timePerStep * (this.total - this.current)); this.printLine() //Stop if finished if (this.autoStop && (this.current / this.total >= 1)) { this.stop() } this.lastUpdate = now; return this } /** * Updates the spinner if enabled */ private updateSpinner = () => { if (this.spinner.currentFrame === undefined) { this.spinner.currentFrame = 0 } this.spinner.currentFrame = (this.spinner.currentFrame + 1) % this.spinner.frames.length; this.currentSpinnerSymbol = this.spinner.frames[this.spinner.currentFrame]; this.printLine(); this.spinnerTimeout = setTimeout(this.updateSpinner, this.spinner.interval); } /** * Formats a time span (given in milliseconds) to a easy human readable string * @param millis timespan in milliseconds */ private formatTime(millis: number): string { //Milliseconds if (millis < 500) { return `${Math.round(millis)}ms`; } //Seconds if (millis < 60 * 1000) { return `${Math.round(millis / 1000)}s`; } //Minutes if (millis < 60 * 60 * 1000) { let minutes = Math.round(millis / (1000 * 60)); let seconds = Math.round(millis % (1000 * 60) / 1000); return `${minutes}m ${seconds}s`; } //Hours if (millis < 24 * 60 * 60 * 1000) { let hours = Math.round(millis / (1000 * 60 * 60)); let minutes = Math.round(millis % (1000 * 60 * 60) / (1000 * 60)); return `${hours}h ${minutes}m`; } //Days let days = Math.round(millis / (1000 * 60 * 60 * 24)); let hours = Math.round(millis % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)); let minutes = Math.round(millis % (1000 * 60 * 60) / (1000 * 60)); return `${days}d ${hours}h ${minutes}m`; } /** * Stop the progressbar * This will stop the spinner and change it's symbol to a checkmark (if not disabled) * Message will be changed to a string describing the elapsed time (if not disabled) * This function will be triggered automatically if the progressbar reaches 100% (if not disabled) * @param withInfo wether to auto-update the progressbar's spinner and message after stopping */ public stop(withInfo = true) { //Stop the spinner if (this.spinnerTimeout) { clearTimeout(this.spinnerTimeout); } if (withInfo) { //change spinner to checkmark this.currentSpinnerSymbol = colorCodes.Green + colorCodes.Bright + "✓" + colorCodes.Reset; if (process.platform === 'win32') { this.currentSpinnerSymbol = "OK " }; //set overalltime to really elapsed time this.overallTime = this.elapsed; this.message = `Finished in ${this.formatTime(this.overallTime)}` this.printLine(); } //add newline and re-enable cursor console.log(this.hideCursor ? colorCodes.ShowCursor : ""); } } interface ProgressbarOptions { /** * Format of the displayed progressbar * Use serveral of this placeholders: * #bar #count #percent #overall #elapsed #ttf #message #spinner * And combine with serveral of this formatters: * ##default ##green ##blue ##red ##yellow ##bright ##dim * @default '#percent #bar' * @example '##bright##blue#spinner##default #percent #bar Elapsed: #elapsed Time to finish: #ttf #message' */ formatString?: string; /** * Number of steps to finish progress * @default 100 */ total?: number; /** * Startdate to calculate elapsed time (in milliseconds) * Use this if the progress started before initialising the progressbar * @default 'new Date().getTime()' */ startDate?: number; /** * Stream to print the progressbar * @default process.stdout */ stream?: NodeJS.ReadWriteStream; /** * Width of the progress bar (only the #bar part) * @default 20 */ width?: number; /** * Symbol for the done progress in the #bar part * @default '>' */ doneSymbol?: string; /** * Symbol for the undone progress in the #bar part * @default '-' */ undoneSymbol?: string; /** * Wether to print to configured stream or not * If set to false get the currently rendered statusbar with bar.renderLine() * @default true */ print?: boolean; /** * Start value of progress * @default 0 */ start?: number; /** * Wether to enable the spinner update function or not. * If enabled the statusbar will re-render automatically every few seconds to update the spinner symbol * Make sure to include #spinner in formatString to use spinner symbol * @default false */ enableSpinner?: boolean; /** * Which timespan to use for timing calculation - If you are unsure allways use false here! * set to FALSE: Assume that each of the remaining steps will take as long as THE AVERAGE OF ALL the previous steps * set to TRUE: Assume that eah of the remaining steps will take as long as THE LAST STEP took. WARNING: This implies, that every call of the ProgressBar.update() function increment the state with the same stepwidth. * @default false using overall elapsed time for timing calculation */ lastUpdateForTiming?: boolean; /** * wether to call progressbar's stop() function automatically if the progress reaches 100% * @default true */ autoStop?: boolean; /** * wether to hide the terminal's cursor while displaying the progress bar * cursor will be re-enabled by the bar.stop() function * @default false */ hideCursor?: boolean; } //ColorCodes from https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color const colorCodes = { Reset: "\x1b[0m", Bright: "\x1b[1m", Dim: "\x1b[2m", Underscore: "\x1b[4m", Blink: "\x1b[5m", Reverse: "\x1b[7m", Hidden: "\x1b[8m", Black: "\x1b[30m", Red: "\x1b[31m", Green: "\x1b[32m", Yellow: "\x1b[33m", Blue: "\x1b[34m", Magenta: "\x1b[35m", Cyan: "\x1b[36m", White: "\x1b[37m", HideCursor: "\x1B[?25l", ShowCursor: "\x1B[?25h", } export let defaultSpinner: Spinner = { interval: 120, frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] } export let simpleSpinner: Spinner = { interval: 120, frames: ["-", "\\", "|", "/"] } interface Spinner { /** * Number of milliseconds to update to the next spinner frame */ interval: number; /** * Array of the spinner "frames" * A frame means one char */ frames: string[]; /** * Used in runtime to store currently displayed frame * @default 0 */ currentFrame?: number; }