-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
585 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,292 @@ | ||
import EventEmitter from 'events'; | ||
|
||
import stringWidth from 'string-width'; | ||
|
||
import type { ProgressBarOption } from './types'; | ||
|
||
import { Terminal } from './terminal'; | ||
import { formatTime, formatBar, formatValue } from './format'; | ||
|
||
export class ProgressBar<P> extends EventEmitter { | ||
private readonly terminal: Terminal; | ||
|
||
private readonly options: ProgressBarOption; | ||
|
||
private readonly schedulingRate: number; | ||
|
||
private value = 0; | ||
|
||
private startValue = 0; | ||
|
||
private payload: P | undefined; | ||
|
||
private total = 100; | ||
|
||
private lastDrawnString: string | undefined = undefined; | ||
|
||
private lastRedraw = Date.now(); | ||
|
||
private isActive = false; | ||
|
||
private timer: NodeJS.Timeout | undefined = undefined; | ||
|
||
constructor(options: ProgressBarOption = {}) { | ||
super(); | ||
|
||
this.options = options; | ||
this.terminal = new Terminal(options.stream ?? process.stderr); | ||
this.schedulingRate = this.terminal.isTTY | ||
? this.options.throttleTime ?? 100 | ||
: this.options.notTTYSchedule ?? 2000; | ||
|
||
if (!options.align) { | ||
options.align = 'left'; | ||
} | ||
if (!options.autopaddingChar) { | ||
options.autopaddingChar = ' '; | ||
} | ||
} | ||
|
||
// // start the progress bar | ||
// start(total, startValue, payload) { | ||
// // progress updates are only visible in TTY mode! | ||
// if (this.options.noTTYOutput === false && this.terminal.isTTY() === false) { | ||
// return; | ||
// } | ||
|
||
// // add handler to restore cursor settings (stop the bar) on SIGINT/SIGTERM ? | ||
// if (this.sigintCallback === null && this.options.gracefulExit) { | ||
// this.sigintCallback = this.stop.bind(this); | ||
// process.once('SIGINT', this.sigintCallback); | ||
// process.once('SIGTERM', this.sigintCallback); | ||
// } | ||
|
||
// // save current cursor settings | ||
// this.terminal.cursorSave(); | ||
|
||
// // hide the cursor ? | ||
// if (this.options.hideCursor === true) { | ||
// this.terminal.cursor(false); | ||
// } | ||
|
||
// // disable line wrapping ? | ||
// if (this.options.linewrap === false) { | ||
// this.terminal.lineWrapping(false); | ||
// } | ||
|
||
// // initialize bar | ||
// super.start(total, startValue, payload); | ||
|
||
// // redraw on start! | ||
// this.render(); | ||
// } | ||
|
||
// // stop the bar | ||
// stop() { | ||
// // timer inactive ? | ||
// if (!this.timer) { | ||
// return; | ||
// } | ||
|
||
// // remove sigint listener | ||
// if (this.sigintCallback) { | ||
// process.removeListener('SIGINT', this.sigintCallback); | ||
// process.removeListener('SIGTERM', this.sigintCallback); | ||
// this.sigintCallback = null; | ||
// } | ||
|
||
// // trigger final rendering | ||
// this.render(); | ||
|
||
// // restore state | ||
// super.stop(); | ||
|
||
// // stop timer | ||
// clearTimeout(this.timer); | ||
// this.timer = null; | ||
|
||
// // cursor hidden ? | ||
// if (this.options.hideCursor === true) { | ||
// this.terminal.cursor(true); | ||
// } | ||
|
||
// // re-enable line wrapping ? | ||
// if (this.options.linewrap === false) { | ||
// this.terminal.lineWrapping(true); | ||
// } | ||
|
||
// // restore cursor on complete (position + settings) | ||
// this.terminal.cursorRestore(); | ||
|
||
// // clear line on complete ? | ||
// if (this.options.clearOnComplete) { | ||
// this.terminal.cursorTo(0, null); | ||
// this.terminal.clearLine(); | ||
// } else { | ||
// // new line on complete | ||
// this.terminal.newline(); | ||
// } | ||
// } | ||
|
||
private format( | ||
params: any, | ||
payload: P | undefined, | ||
options: ProgressBarOption | ||
) { | ||
const template = ` {bar} | {value}/{total}`; | ||
|
||
let s = template; | ||
|
||
// custom time format set ? | ||
// const formatTime = options.formatTime || _defaultFormatTime; | ||
|
||
// custom value format set ? | ||
// const formatValue = options.formatValue || _defaultFormatValue; | ||
|
||
// custom bar format set ? | ||
// const formatBar = options.formatBar || _defaultFormatBar; | ||
|
||
// calculate progress in percent | ||
const percentage = Math.floor(params.progress * 100); | ||
|
||
// bar stopped and stopTime set ? | ||
const stopTime = params.stopTime || Date.now(); | ||
|
||
// calculate elapsed time | ||
const elapsedTime = Math.round((stopTime - params.startTime) / 1000); | ||
|
||
// merges data from payload and calculated | ||
const context = Object.assign({}, payload, { | ||
bar: formatBar(params.progress, options), | ||
|
||
percentage: formatValue(percentage, options, 'percentage'), | ||
total: formatValue(params.total, options, 'total'), | ||
value: formatValue(params.value, options, 'value'), | ||
|
||
// eta: formatValue(params.eta, options, 'eta'), | ||
// eta_formatted: formatTime(params.eta, options, 5), | ||
|
||
duration: formatValue(elapsedTime, options, 'duration'), | ||
duration_formatted: formatTime(elapsedTime, 1, options) | ||
}); | ||
|
||
// assign placeholder tokens | ||
s = s.replace(/\{(\w+)\}/g, function (match, key) { | ||
// key exists within payload/context | ||
// @ts-ignore | ||
if (typeof context[key] !== 'undefined') { | ||
// @ts-ignore | ||
return context[key]; | ||
} | ||
// no changes to unknown values | ||
return match; | ||
}); | ||
|
||
// calculate available whitespace (2 characters margin of error) | ||
const fullMargin = Math.max(0, params.maxWidth - stringWidth(s) - 2); | ||
const halfMargin = Math.floor(fullMargin / 2); | ||
|
||
// distribute available whitespace according to position | ||
switch (options.align) { | ||
// fill start-of-line with whitespaces | ||
case 'right': | ||
s = fullMargin > 0 ? ' '.repeat(fullMargin) + s : s; | ||
break; | ||
|
||
// distribute whitespaces to left+right | ||
case 'center': | ||
s = halfMargin > 0 ? ' '.repeat(halfMargin) + s : s; | ||
break; | ||
|
||
// default: left align, no additional whitespaces | ||
case 'left': | ||
default: | ||
break; | ||
} | ||
|
||
return s; | ||
} | ||
|
||
render() { | ||
// stop timer | ||
if (this.timer) { | ||
clearTimeout(this.timer); | ||
this.timer = undefined; | ||
} | ||
|
||
this.renderTerminal(); | ||
|
||
// add new line in notty mode! | ||
if (this.options.noTTYOutput && this.terminal.isTTY === false) { | ||
this.terminal.newline(); | ||
} | ||
|
||
// setup new timer | ||
this.timer = setTimeout(this.render.bind(this), this.schedulingRate); | ||
} | ||
|
||
private renderTerminal(forceRendering = false) { | ||
const params = { | ||
progress: this.getProgress(), | ||
// startTime: this.startTime, | ||
// stopTime: this.stopTime, | ||
total: this.total, | ||
value: this.value, | ||
maxWidth: this.terminal.getWidth() | ||
}; | ||
|
||
// format string | ||
const text = this.format(params, this.payload, this.options); | ||
|
||
const forceRedraw = | ||
forceRendering || | ||
this.options.forceRedraw || | ||
// force redraw in notty-mode! | ||
(this.options.noTTYOutput && !this.terminal.isTTY); | ||
|
||
if (forceRedraw || this.lastDrawnString != text) { | ||
// trigger event | ||
this.emit('pre:redraw'); | ||
|
||
// set cursor to start of line | ||
this.terminal.cursorTo(0, undefined); | ||
|
||
// write output | ||
this.terminal.write(text); | ||
|
||
// clear to the right from cursor | ||
this.terminal.clearRight(); | ||
|
||
// store string | ||
this.lastDrawnString = text; | ||
|
||
// set last redraw time | ||
this.lastRedraw = Date.now(); | ||
|
||
// trigger event | ||
this.emit('post:redraw'); | ||
} | ||
} | ||
|
||
getProgress() { | ||
// calculate the normalized current progress | ||
let progress = this.value / this.total; | ||
|
||
// use relative progress calculation ? range between startValue and total is then used as 100% | ||
// startValue (offset) is ignored for calculations | ||
// if (this.options.progressCalculationRelative) { | ||
// progress = | ||
// (this.value - this.startValue) / (this.total - this.startValue); | ||
// } | ||
|
||
// // handle NaN Errors caused by total=0. Set to complete in this case | ||
// if (isNaN(progress)) { | ||
// progress = this.options && this.options.emptyOnZero ? 0.0 : 1.0; | ||
// } | ||
|
||
// limiter | ||
progress = Math.min(Math.max(progress, 0.0), 1.0); | ||
|
||
return progress; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import type { ProgressBarOption } from './types'; | ||
|
||
export function formatTime( | ||
t: number, | ||
roundToMultipleOf: number, | ||
options: ProgressBarOption | ||
) { | ||
function round(value: number) { | ||
if (roundToMultipleOf) { | ||
return roundToMultipleOf * Math.round(value / roundToMultipleOf); | ||
} else { | ||
return value; | ||
} | ||
} | ||
|
||
// leading zero padding | ||
function autopadding(v: number) { | ||
return (' ' + v).slice(-2); | ||
} | ||
|
||
// > 1h ? | ||
if (t > 3600) { | ||
return ( | ||
autopadding(Math.floor(t / 3600)) + | ||
'h' + | ||
autopadding(round((t % 3600) / 60)) + | ||
'm' | ||
); | ||
|
||
// > 60s ? | ||
} else if (t > 60) { | ||
return ( | ||
autopadding(Math.floor(t / 60)) + 'm' + autopadding(round(t % 60)) + 's' | ||
); | ||
|
||
// > 10s ? | ||
} else if (t > 10) { | ||
return autopadding(round(t)) + 's'; | ||
|
||
// default: don't apply round to multiple | ||
} else { | ||
return autopadding(t) + 's'; | ||
} | ||
} | ||
|
||
export function formatValue( | ||
v: number, | ||
options: ProgressBarOption, | ||
type: string | ||
) { | ||
return v + ''; | ||
// // no autopadding ? passthrough | ||
// if (options.autopadding !== true) { | ||
// return v; | ||
// } | ||
|
||
// // padding | ||
// function autopadding(value, length) { | ||
// return (options.autopaddingChar + value).slice(-length); | ||
// } | ||
|
||
// switch (type) { | ||
// case 'percentage': | ||
// return autopadding(v, 3); | ||
|
||
// default: | ||
// return v; | ||
// } | ||
} | ||
|
||
export function formatBar(progress: number, options: ProgressBarOption) { | ||
options.barsize = 40; | ||
options.barCompleteString = '='; | ||
options.barGlue = ''; | ||
options.barIncompleteString = '-'; | ||
|
||
// calculate barsize | ||
const completeSize = Math.round(progress * options.barsize); | ||
const incompleteSize = options.barsize - completeSize; | ||
|
||
// generate bar string by stripping the pre-rendered strings | ||
return ( | ||
options.barCompleteString.substr(0, completeSize) + | ||
options.barGlue + | ||
options.barIncompleteString.substr(0, incompleteSize) | ||
); | ||
} |
Empty file.
Oops, something went wrong.