fedtools-utilities
Version:
Set of utilites for fedtools within nodejs
245 lines (210 loc) • 6.1 kB
JavaScript
/*!
* node-progress
* Copyright(c) 2011 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/* fedtools-utilities additions:
- adding extra formatting to timings
- adding extra updates to eta/elapsed even without ticks
- linting code
- code formatting
*/
var moment = require('moment');
require('./moment-duration-format');
/**
* Initialize a `ProgressBar` with the given `fmt` string and `options` or
* `total`.
*
* Options:
*
* - `total` total number of ticks to complete
* - `width` the displayed width of the progress bar defaulting to total
* - `stream` the output stream defaulting to stderr
* - `complete` completion character defaulting to "="
* - `incomplete` incomplete character defaulting to "-"
* - `callback` optional function to call when the progress bar completes
* - `clear` will clear the progress bar upon termination
*
* Tokens:
*
* - `:bar` the progress bar itself
* - `:current` current tick number
* - `:total` total ticks
* - `:elapsed` time elapsed in seconds
* - `:percent` completion percentage
* - `:eta` eta in seconds
*
* @param {string} fmt
* @param {object|number} options or total
* @api public
*/
function ProgressBar(fmt, options) {
this.stream = options.stream || process.stdout;
if (typeof (options) === 'number') {
var total = options;
options = {};
options.total = total;
} else {
options = options || {};
if ('string' !== typeof fmt) {
throw new Error('format required');
}
if ('number' !== typeof options.total) {
throw new Error('total required');
}
}
this.fmt = fmt;
this.curr = 0;
this.total = options.total;
this.width = options.width || this.total;
this.clear = options.clear;
this.chars = {
complete: options.complete || '=',
incomplete: options.incomplete || '-'
};
this.callback = options.callback || function () {};
this.lastDraw = '';
this.format = options.format || null;
}
/**
* "tick" the progress bar with optional `len` and optional `tokens`.
*
* @param {number|object} len or tokens
* @param {object} tokens
* @api public
*/
ProgressBar.prototype.tick = function (len, tokens) {
var self = this;
if (len !== 0) {
len = len || 1;
}
// swap tokens
if ('object' === typeof len) {
tokens = len;
len = 1;
}
// start time for eta
if (0 === self.curr) {
self.start = new Date();
}
self.curr += len;
self.render(tokens);
// need to setup an automatic update of the eta and elapsed
// tokens, even without any ticks.
if (self.interval) {
clearInterval(self.interval);
}
if (self.curr < self.total - 1) {
self.interval = setInterval(function () {
self.render(tokens);
}, 1000);
}
// progress complete
if (self.curr >= self.total) {
self.complete = true;
self.terminate();
self.callback(self);
return;
}
};
ProgressBar.prototype._formatTime = function (time) {
if (isNaN(time) || !isFinite(time)) {
time = '0.0';
} else {
time = (time / 1000).toFixed(1);
}
if (this.format && typeof this.format === 'string') {
time = moment.duration(time * 1000, 'milliseconds').format(this.format, {
trim: false
});
}
return time;
};
/**
* Method to render the progress bar with optional `tokens` to place in the
* progress bar's `fmt` field.
*
* @param {object} tokens
* @api public
*/
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;
var elapsed = new Date() - this.start;
var eta = (percent === 100) ? 0 : elapsed * (this.total / this.curr - 1);
elapsed = this._formatTime(elapsed);
eta = this._formatTime(eta);
/* populate the bar template with percentages and timestamps */
var str = this.fmt
.replace(':current', this.curr)
.replace(':total', this.total)
.replace(':elapsed', elapsed)
.replace(':eta', (percent.toFixed(0) > 15) ? eta : '***')
.replace(':percent', percent.toFixed(0) + '%');
/* 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 = new Array(completeLength + 1).join(this.chars.complete);
incomplete = new Array(width - completeLength + 1).join(this.chars.incomplete);
/* fill in the actual progress bar */
str = str.replace(':bar', complete + incomplete);
/* replace the extra tokens */
if (tokens) {
for (var key in tokens) {
if (tokens.hasOwnProperty(key)) {
str = str.replace(':' + key, tokens[key]);
}
}
}
if (this.lastDraw !== str) {
this.stream.clearLine();
this.stream.cursorTo(0);
this.stream.write(str);
this.lastDraw = str;
}
};
/**
* "update" the progress bar to represent an exact percentage.
* The ratio (between 0 and 1) specified will be multiplied by `total` and
* floored, representing the closest available "tick." For example, if a
* progress bar has a length of 3 and `update(0.5)` is called, the progress
* will be set to 1.
*
* A ratio of 0.5 will attempt to set the progress to halfway.
*
* @param {number} ratio The ratio (between 0 and 1 inclusive) to set the
* overall completion to.
* @api public
*/
ProgressBar.prototype.update = function (ratio, tokens) {
var goal = Math.floor(ratio * this.total);
var delta = goal - this.curr;
this.tick(delta, tokens);
};
/**
* Terminates a progress bar.
*
* @api public
*/
ProgressBar.prototype.terminate = function () {
if (this.interval) {
clearInterval(this.interval);
}
if (this.clear) {
this.stream.clearLine();
this.stream.cursorTo(0);
} else {
console.log();
}
};
/**
* Expose `ProgressBar`.
*/
exports = module.exports = ProgressBar;