Skip to content

Commit

Permalink
Add support for indeterminable progress bar (unknown total)
Browse files Browse the repository at this point in the history
Changes:
 * new `.done()` method to complete the progress
 * new `.updates` counter to track `.render()` calls
 * support for non-positive `.total`
 * no more `.tick()` calls after progress bar is complete and terminated
  • Loading branch information
lbeschastny committed May 22, 2016
1 parent d479135 commit d7c3653
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 26 deletions.
2 changes: 1 addition & 1 deletion examples/backnforth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
24 changes: 24 additions & 0 deletions examples/indeterminable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* An example to show how node-progress handles user-specified widths
* which exceed the number of columns in the terminal
*/

var ProgressBar = require('../');

// simulated download, passing the chunk lengths to tick()

var bar = new ProgressBar(' [: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);
}
})();
90 changes: 65 additions & 25 deletions lib/node-progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ function ProgressBar(fmt, options) {

this.fmt = fmt;
this.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 || '=',
Expand All @@ -76,6 +77,8 @@ function ProgressBar(fmt, options) {
*/

ProgressBar.prototype.tick = function(len, tokens){
if (this.complete) return;

if (len !== 0)
len = len || 1;

Expand All @@ -94,13 +97,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);
};

/**
Expand All @@ -119,34 +132,57 @@ 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 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(':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 + '%');

/* compute the available space (non-zero) for the bar */
var availableSpace = Math.max(0, this.stream.columns - str.replace(':bar', '').length);
var width = Math.min(this.width, availableSpace);

/* TODO: the following assumes the user has one ':bar' token */
completeLength = Math.round(width * ratio);
complete = Array(completeLength + 1).join(this.chars.complete);
incomplete = Array(width - completeLength + 1).join(this.chars.incomplete);
var bar;
if (this.total > 0) {
var incomplete, complete, completeLength;
completeLength = Math.round(width * ratio);
complete = Array(completeLength + 1).join(this.chars.complete);
incomplete = Array(width - completeLength + 1).join(this.chars.incomplete);
bar = complete + incomplete;
} else if (this.complete) {
// complete indeterminable progress bar
bar = Array(width + 1).join(this.chars.complete);
} 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]);
Expand Down Expand Up @@ -174,10 +210,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);
}
};

/**
Expand Down

0 comments on commit d7c3653

Please sign in to comment.