Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature proposal - indeterminable progress bar (unknown total) #121

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
22 changes: 22 additions & 0 deletions examples/indeterminable.js
Original file line number Diff line number Diff line change
@@ -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);
}
})();
105 changes: 74 additions & 31 deletions lib/node-progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if Infinity is a good default here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of this change?

Copy link
Author

@lbeschastny lbeschastny Dec 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my proposal this.total could be non-positive, but we can't use non-positive value as a default for width.

So, we need a sane default value. Infinity here means that progress bar will attempt to use all available space.

this.clear = options.clear
this.chars = {
complete : options.complete || '=',
Expand All @@ -80,6 +82,8 @@ function ProgressBar(fmt, options) {
*/

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

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

Expand All @@ -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);
};

/**
Expand All @@ -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]);
Expand Down Expand Up @@ -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);
}
};

/**
Expand Down