diff --git a/Readme.md b/Readme.md index b5a4b1f..3fc5413 100644 --- a/Readme.md +++ b/Readme.md @@ -46,6 +46,7 @@ These are keys in the options object you can pass to the progress bar along with These are tokens you can use in the format of your progress bar. - `:bar` the progress bar itself +- `:wheel` rotating progress indicator - `:current` current tick number - `:total` total ticks - `:elapsed` time elapsed in seconds diff --git a/examples/backnforth.js b/examples/backnforth.js index ffc0dc6..e10f2a6 100644 --- a/examples/backnforth.js +++ b/examples/backnforth.js @@ -26,7 +26,7 @@ function forward() { function backward() { bar.tick(-1, { title: 'backward' }); if (bar.curr == 0) { - bar.terminate(); + bar.done(); } else { setTimeout(backward, 20); } diff --git a/examples/indeterminable.js b/examples/indeterminable.js new file mode 100644 index 0000000..6e325f3 --- /dev/null +++ b/examples/indeterminable.js @@ -0,0 +1,22 @@ +/** + * An example to show how node-progress handles progress bar + * with unknown total number of ticks + */ + +var ProgressBar = require('../'); + +var bar = new ProgressBar(' [:wheel][:bar] :current/:total :elapseds :percent :etas', { + complete: '=' + , incomplete: ' ' + , width: 50 + , total: -1 // total number of ticks is unknown +}); + +(function next() { + bar.tick(1); + if (bar.curr >= 150) { + bar.done(); + } else { + setTimeout(next, 50); + } +})(); diff --git a/lib/node-progress.js b/lib/node-progress.js index 6480ffa..737c800 100644 --- a/lib/node-progress.js +++ b/lib/node-progress.js @@ -30,6 +30,7 @@ exports = module.exports = ProgressBar; * Tokens: * * - `:bar` the progress bar itself + * - `:wheel` rotating progress indicator * - `:current` current tick number * - `:total` total ticks * - `:elapsed` time elapsed in seconds @@ -57,8 +58,9 @@ function ProgressBar(fmt, options) { this.fmt = fmt; this.curr = options.curr || 0; + this.updates = 0; this.total = options.total; - this.width = options.width || this.total; + this.width = options.width || (this.total > 0 ? this.total : Infinity); this.clear = options.clear this.chars = { complete : options.complete || '=', @@ -80,6 +82,8 @@ function ProgressBar(fmt, options) { */ ProgressBar.prototype.tick = function(len, tokens){ + if (this.complete) return; + if (len !== 0) len = len || 1; @@ -98,13 +102,23 @@ ProgressBar.prototype.tick = function(len, tokens){ } // progress complete - if (this.curr >= this.total) { - if (this.renderThrottleTimeout) this.render(); - this.complete = true; - this.terminate(); - this.callback(this); - return; - } + if (this.total > 0 && this.curr >= this.total) this.done(); +}; + +/** + * complete the progress bar with optional `tokens`. + * + * @param {object} tokens + * @api public + */ + +ProgressBar.prototype.done = function(tokens){ + if (tokens) this.tokens = tokens; + + this.complete = true; + this.render(); + this.terminate(); + this.callback(this); }; /** @@ -123,44 +137,69 @@ ProgressBar.prototype.render = function (tokens) { if (!this.stream.isTTY) return; - var ratio = this.curr / this.total; - ratio = Math.min(Math.max(ratio, 0), 1); - - var percent = ratio * 100; - var incomplete, complete, completeLength; + this.updates++; var elapsed = new Date - this.start; - var eta = (percent == 100) ? 0 : elapsed * (this.total / this.curr - 1); var rate = this.curr / (elapsed / 1000); + var ratio, eta, percent; + if (this.total > 0) { + ratio = this.curr / this.total; + ratio = Math.min(Math.max(ratio, 0), 1); + percent = ratio * 100; + eta = (percent == 100) ? 0 : elapsed * (this.total / this.curr - 1); + eta = (isNaN(eta) || !isFinite(eta)) ? '0.0' : (eta / 1000).toFixed(1); + percent = percent.toFixed(0); + } else { + // indeterminable progress bar (unknown total) + percent = this.complete ? '100' : '?'; + eta = this.complete ? '0.0' : '?'; + } /* populate the bar template with percentages and timestamps */ var str = this.fmt + .replace(':wheel', this.complete ? '+' : ['/', '-', '\\', '|'][this.updates % 4]) .replace(':current', this.curr) - .replace(':total', this.total) + .replace(':total', this.total > 0 ? this.total : '?') .replace(':elapsed', isNaN(elapsed) ? '0.0' : (elapsed / 1000).toFixed(1)) - .replace(':eta', (isNaN(eta) || !isFinite(eta)) ? '0.0' : (eta / 1000) - .toFixed(1)) - .replace(':percent', percent.toFixed(0) + '%') + .replace(':eta', eta) + .replace(':percent', percent + '%') .replace(':rate', Math.round(rate)); /* compute the available space (non-zero) for the bar */ var availableSpace = Math.max(0, this.stream.columns - str.replace(':bar', '').length); if(availableSpace && process.platform === 'win32'){ - availableSpace = availableSpace - 1; + availableSpace = availableSpace - 1; } - + var width = Math.min(this.width, availableSpace); /* TODO: the following assumes the user has one ':bar' token */ - completeLength = Math.round(width * ratio); - complete = Array(Math.max(0, completeLength + 1)).join(this.chars.complete); - incomplete = Array(Math.max(0, width - completeLength + 1)).join(this.chars.incomplete); - - /* add head to the complete string */ - if(completeLength > 0) - complete = complete.slice(0, -1) + this.chars.head; + var bar; + if (this.complete) { + // complete progress bar + bar = Array(width + 1).join(this.chars.complete); + } else if (this.total > 0) { + var incomplete, complete, completeLength; + completeLength = Math.round(width * ratio); + complete = Array(Math.max(0, completeLength + 1)).join(this.chars.complete); + incomplete = Array(Math.max(0, width - completeLength + 1)).join(this.chars.incomplete); + + /* add head to the complete string */ + if(completeLength > 0) + complete = complete.slice(0, -1) + this.chars.head; + + bar = complete + incomplete; + } else { + // incomplete indeterminable progress bar + bar = Array(width); + for (var i = 0; i < width; i++) { + var dist = (((i - this.updates) % width) + width) % width; // (i - this.updates) mod width + bar[i] = dist < 3 ? this.chars.complete : this.chars.incomplete; + } + bar = bar.join(''); + } /* fill in the actual progress bar */ - str = str.replace(':bar', complete + incomplete); + str = str.replace(':bar', bar); /* replace the extra tokens */ if (this.tokens) for (var key in this.tokens) str = str.replace(':' + key, this.tokens[key]); @@ -188,10 +227,14 @@ ProgressBar.prototype.render = function (tokens) { */ ProgressBar.prototype.update = function (ratio, tokens) { - var goal = Math.floor(ratio * this.total); - var delta = goal - this.curr; + if (this.total > 0) { + var goal = Math.floor(ratio * this.total); + var delta = goal - this.curr; - this.tick(delta, tokens); + this.tick(delta, tokens); + } else if (ratio == 1) { + this.done(tokens); + } }; /**