Skip to content

Commit

Permalink
Progress bar improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin committed Nov 20, 2016
1 parent 26f089c commit 020f822
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 138 deletions.
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@

# Main code, tests, and docs
!/lib/
!/deps/
!/test/
!/doc/
!/tools/
!/setup/

# Exclude most packages under node_modules because they are dev dependencies
!/node_modules/
/node_modules/*/

# This one is actually a runtime dependency
!/node_modules/progress/
# Exclude packages under node_modules because they are dev dependencies
/node_modules/

# Default version link
/default
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
31 changes: 29 additions & 2 deletions node_modules/progress/Readme.md → deps/progress/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ These are keys in the options object you can pass to the progress bar along with
- `stream` the output stream defaulting to stderr
- `complete` completion character defaulting to "="
- `incomplete` incomplete character defaulting to "-"
- `renderThrottle` minimum time between updates in milliseconds defaulting to 16
- `clear` option to clear the bar on completion defaulting to false
- `callback` optional function to call when the progress bar completes

Expand All @@ -49,6 +50,32 @@ These are tokens you can use in the format of your progress bar.
- `:percent` completion percentage
- `:eta` estimated completion time in seconds

Tokens other than `:bar` may include a suffix to specify width, that is a minus
(for left-padding) or plus (for right-padding) followed by an integer, for example
`:percent-4`.

### Custom Tokens

You can define custom tokens by adding a `{'name': value}` object parameter to your method (`tick()`, `update()`, etc.) calls.

```javascript
var bar = new ProgressBar(':current: :token1 :token2', { total: 3 })
bar.tick({
'token1': "Hello",
'token2': "World!\n"
})
bar.tick(2, {
'token1': "Goodbye",
'token2': "World!"
})
```
The above example would result in the output below.

```
1: Hello World!
3: Goodbye World!
```

## Examples

### Download
Expand All @@ -71,7 +98,7 @@ req.on('response', function(res){
var len = parseInt(res.headers['content-length'], 10);

console.log();
var bar = new ProgressBar(' downloading [:bar] :percent :etas', {
var bar = new ProgressBar(' downloading [:bar] :percent-4 :eta-3s', {
complete: '=',
incomplete: ' ',
width: 20,
Expand All @@ -93,7 +120,7 @@ req.end();
The above example result in a progress bar like the one below.

```
downloading [===== ] 29% 3.7s
downloading [===== ] 29% 3.7s
```

You can see more examples in the `examples` folder.
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports = module.exports = ProgressBar;
* - `stream` the output stream defaulting to stderr
* - `complete` completion character defaulting to "="
* - `incomplete` incomplete character defaulting to "-"
* - `renderThrottle` minimum time between updates in milliseconds defaulting to 16
* - `callback` optional function to call when the progress bar completes
* - `clear` will clear the progress bar upon termination
*
Expand All @@ -33,6 +34,10 @@ exports = module.exports = ProgressBar;
* - `:percent` completion percentage
* - `:eta` eta in seconds
*
* Tokens other than `:bar` may include a suffix to specify width, that is a minus
* (for left-padding) or plus (for right-padding) followed by an integer, for example
* `:percent-4`.
*
* @param {string} fmt
* @param {object|number} options or total
* @api public
Expand Down Expand Up @@ -60,7 +65,9 @@ function ProgressBar(fmt, options) {
complete : options.complete || '=',
incomplete : options.incomplete || '-'
};
this.renderThrottle = options.renderThrottle !== 0 ? (options.renderThrottle || 16) : 0;
this.callback = options.callback || function () {};
this.tokens = {};
this.lastDraw = '';
}

Expand All @@ -78,15 +85,21 @@ ProgressBar.prototype.tick = function(len, tokens){

// swap tokens
if ('object' == typeof len) tokens = len, len = 1;
if (tokens) this.tokens = tokens;

// start time for eta
if (0 == this.curr) this.start = new Date;

this.curr += len
this.render(tokens);

// schedule render
if (!this.renderThrottleTimeout) {
this.renderThrottleTimeout = setTimeout(this.render.bind(this), this.renderThrottle);
}

// progress complete
if (this.curr >= this.total) {
if (!this.complete && this.curr >= this.total) {
if (this.renderThrottleTimeout) this.render();
this.complete = true;
this.terminate();
this.callback(this);
Expand All @@ -103,6 +116,11 @@ ProgressBar.prototype.tick = function(len, tokens){
*/

ProgressBar.prototype.render = function (tokens) {
clearTimeout(this.renderThrottleTimeout);
this.renderThrottleTimeout = null;

if (tokens) this.tokens = tokens;

if (!this.stream.isTTY) return;

var ratio = this.curr / this.total;
Expand All @@ -114,13 +132,14 @@ ProgressBar.prototype.render = function (tokens) {
var eta = (percent == 100) ? 0 : elapsed * (this.total / this.curr - 1);

/* populate the bar template with percentages and timestamps */
var str = this.fmt
.replace(':current', this.curr)
.replace(':total', 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) + '%');
var str = this.fmt;
str = replaceToken(str, 'current', this.curr);
str = replaceToken(str, 'total', this.total);
str = replaceToken(str, 'elapsed', isNaN(elapsed) ? '0.0'
: elapsed >= 10000 ? Math.round(elapsed / 1000) : (elapsed / 1000).toFixed(1));
str = replaceToken(str, 'eta', (isNaN(eta) || !isFinite(eta)) ? '0.0'
: eta >= 10000 ? Math.round(eta / 1000) : (eta / 1000).toFixed(1));
str = replaceToken(str, '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);
Expand All @@ -135,16 +154,54 @@ ProgressBar.prototype.render = function (tokens) {
str = str.replace(':bar', complete + incomplete);

/* replace the extra tokens */
if (tokens) for (var key in tokens) str = str.replace(':' + key, tokens[key]);
if (this.tokens) for (var key in this.tokens) str = replaceToken(str, key, this.tokens[key]);

if (this.lastDraw !== str) {
this.stream.clearLine();
this.stream.cursorTo(0);
this.stream.write(str);
if (str.length < this.lastDraw.length) {
// Reduce flicker - don't clear unless the new line is shorter.
this.stream.clearLine(1);
}
this.lastDraw = str;
}
};

/**
* Replace a token in a string, using optional width specifiers after the token.
* @param str {string} The string that may contain the token to be replaced.
* @param token {string} Token to replace, not including the ':' prefix or width suffix.
* @param value {string} The replacement value.
* @return The resulting string after replacement.
*/
function replaceToken(str, token, value) {
token = ':' + token;
var tokenIndex = str.indexOf(token);
if (tokenIndex < 0) {
return str;
}

value = (value ? value.toString() : '');

function repeat(s, n) { return n <= 0 ? '' : Array(n + 1).join(s); };

if (str[tokenIndex + token.length] === '-') {
width = parseInt(str.substr(tokenIndex + token.length + 1));
if (width) {
token = token + '-' + width;
value = repeat(' ', width - value.length) + value;
}
} else if (str[tokenIndex + token.length] === '+') {
width = parseInt(str.substr(tokenIndex + token.length + 1));
if (width) {
token = token + '+' + width;
value = value + repeat(' ', width - value.length);
}
}

return str.replace(token, value);
}

/**
* "update" the progress bar to represent an exact percentage.
* The ratio (between 0 and 1) specified will be multiplied by `total` and
Expand Down Expand Up @@ -176,5 +233,5 @@ ProgressBar.prototype.terminate = function () {
if (this.clear) {
this.stream.clearLine();
this.stream.cursorTo(0);
} else console.log();
} else this.stream.write('\n');
};
44 changes: 44 additions & 0 deletions deps/progress/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "progress",
"version": "1.2.0",
"description": "Flexible ascii progress bar",
"keywords": [
"cli",
"progress"
],
"author": {
"name": "TJ Holowaychuk",
"email": "tj@vision-media.ca"
},
"contributors": [
{
"name": "Christoffer Hallas",
"email": "christoffer.hallas@gmail.com"
},
{
"name": "Jordan Scales",
"email": "scalesjordan@gmail.com"
}
],
"dependencies": {},
"main": "index",
"engines": {
"node": ">=0.4.0"
},
"repository": {
"type": "git",
"url": "git://github.com/visionmedia/node-progress.git"
},
"license": "MIT",
"gitHead": "4a6c2fdc782cff6c9f8933339e7aae0b38d682d4",
"readme": "Flexible ascii progress bar.\r\n\r\n## Installation\r\n\r\n```bash\r\n$ npm install progress\r\n```\r\n\r\n## Usage\r\n\r\nFirst we create a `ProgressBar`, giving it a format string\r\nas well as the `total`, telling the progress bar when it will\r\nbe considered complete. After that all we need to do is `tick()` appropriately.\r\n\r\n```javascript\r\nvar ProgressBar = require('progress');\r\n\r\nvar bar = new ProgressBar(':bar', { total: 10 });\r\nvar timer = setInterval(function () {\r\n bar.tick();\r\n if (bar.complete) {\r\n console.log('\\ncomplete\\n');\r\n clearInterval(timer);\r\n }\r\n}, 100);\r\n```\r\n\r\n### Options\r\n\r\nThese are keys in the options object you can pass to the progress bar along with\r\n`total` as seen in the example above.\r\n\r\n- `total` total number of ticks to complete\r\n- `width` the displayed width of the progress bar defaulting to total\r\n- `stream` the output stream defaulting to stderr\r\n- `complete` completion character defaulting to \"=\"\r\n- `incomplete` incomplete character defaulting to \"-\"\r\n- `renderThrottle` minimum time between updates in milliseconds defaulting to 16\r\n- `clear` option to clear the bar on completion defaulting to false\r\n- `callback` optional function to call when the progress bar completes\r\n\r\n### Tokens\r\n\r\nThese are tokens you can use in the format of your progress bar.\r\n\r\n- `:bar` the progress bar itself\r\n- `:current` current tick number\r\n- `:total` total ticks\r\n- `:elapsed` time elapsed in seconds\r\n- `:percent` completion percentage\r\n- `:eta` estimated completion time in seconds\r\n\r\nTokens other than `:bar` may include a suffix to specify width, that is a minus\r\n(for left-padding) or plus (for right-padding) followed by an integer, for example\r\n`:percent-4`.\r\n\r\n### Custom Tokens\r\n\r\nYou can define custom tokens by adding a `{'name': value}` object parameter to your method (`tick()`, `update()`, etc.) calls.\r\n\r\n```javascript\r\nvar bar = new ProgressBar(':current: :token1 :token2', { total: 3 })\r\nbar.tick({\r\n 'token1': \"Hello\",\r\n 'token2': \"World!\\n\"\r\n})\r\nbar.tick(2, {\r\n 'token1': \"Goodbye\",\r\n 'token2': \"World!\"\r\n})\r\n```\r\nThe above example would result in the output below.\r\n\r\n```\r\n1: Hello World!\r\n3: Goodbye World!\r\n```\r\n\r\n## Examples\r\n\r\n### Download\r\n\r\nIn our download example each tick has a variable influence, so we pass the chunk\r\nlength which adjusts the progress bar appropriately relative to the total\r\nlength.\r\n\r\n```javascript\r\nvar ProgressBar = require('../');\r\nvar https = require('https');\r\n\r\nvar req = https.request({\r\n host: 'download.github.com',\r\n port: 443,\r\n path: '/visionmedia-node-jscoverage-0d4608a.zip'\r\n});\r\n\r\nreq.on('response', function(res){\r\n var len = parseInt(res.headers['content-length'], 10);\r\n\r\n console.log();\r\n var bar = new ProgressBar(' downloading [:bar] :percent-4 :eta-3s', {\r\n complete: '=',\r\n incomplete: ' ',\r\n width: 20,\r\n total: len\r\n });\r\n\r\n res.on('data', function (chunk) {\r\n bar.tick(chunk.length);\r\n });\r\n\r\n res.on('end', function () {\r\n console.log('\\n');\r\n });\r\n});\r\n\r\nreq.end();\r\n```\r\n\r\nThe above example result in a progress bar like the one below.\r\n\r\n```\r\ndownloading [===== ] 29% 3.7s\r\n```\r\n\r\nYou can see more examples in the `examples` folder.\r\n\r\n## License\r\n\r\nMIT\r\n",
"readmeFilename": "Readme.md",
"bugs": {
"url": "https://github.com/visionmedia/node-progress/issues"
},
"homepage": "https://github.com/visionmedia/node-progress#readme",
"_id": "progress@1.2.0",
"_shasum": "e9185384798a6911b3c37945b4dcf3ad28c13e46",
"_from": "git://github.com/jasongin/node-progress.git",
"_resolved": "git://github.com/jasongin/node-progress.git#4a6c2fdc782cff6c9f8933339e7aae0b38d682d4"
}
7 changes: 5 additions & 2 deletions lib/addRemove.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ const Error = require('./error');
let nvsList = require('./list'); // Non-const enables test mocking
let nvsUse = require('./use'); // Non-const enables test mocking
let nvsLink = require('./link'); // Non-const enables test mocking
let nvsDownload = require('./download'); // Non-const enables test mocking
let nvsExtract = require('./extract'); // Non-const enables test mocking
const NodeVersion = require('./version');

let nvsDownload = null; // Delay-load
let nvsExtract = null; // Delay-load

/**
* Downloads and extracts a version of node.
*/
Expand Down Expand Up @@ -72,12 +73,14 @@ function downloadAndExtractAsync(version, remoteUri) {
version.semanticVersion,
version.arch);

nvsDownload = nvsDownload || require('./download');
return nvsDownload.ensureFileCachedAsync(
archiveFileName,
archiveFileUri,
shasumFileName,
shasumFileUri
).then(zipFilePath => {
nvsExtract = nvsExtract || require('./extract');
return nvsExtract.extractAsync(zipFilePath, targetDir);
}).then(() => {
// Guess the name of the top-level extracted directory.
Expand Down
8 changes: 4 additions & 4 deletions lib/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const crypto = require('crypto');
const path = require('path');
const stream = require('stream');
const ProgressBar = require('progress');
const ProgressBar = require('../deps/progress');

let fs = require('fs'); // Non-const enables test mocking
let http = require('http'); // Non-const enables test mocking
Expand All @@ -14,7 +14,7 @@ function downloadFileAsync(filePath, fileUri) {
let stream = null;
return new Promise((resolve, reject) => {
try {
const progressFormat = 'Downloading [:bar] :percent :etas ';
const progressFormat = 'Downloading [:bar] :percent-4 :eta-3s ';
stream = fs.createWriteStream(filePath);

if (path.isAbsolute(fileUri)) {
Expand All @@ -41,7 +41,7 @@ function downloadFileAsync(filePath, fileUri) {
client.get(fileUri, (res) => {
if (res.statusCode === 200) {
let totalBytes = parseInt(res.headers['content-length'], 10);
let progressFormat = 'Downloading [:bar] :percent :etas ';
let progressFormat = 'Downloading [:bar] :percent-4 :eta-3s ';
if (!settings.quiet && totalBytes > 100000) {
res.pipe(streamProgress(progressFormat, {
complete: '#',
Expand Down Expand Up @@ -163,7 +163,7 @@ function streamProgress(progressFormat, options) {
let progressBar = new ProgressBar(progressFormat, options);
passThrough.on('data', chunk => {
if (progressBar.curr + chunk.length >= progressBar.total) {
let finalFormat = progressFormat.replace(/:etas/, ' ');
let finalFormat = progressFormat.replace(/:eta-3s/, ' ');
progressBar.fmt = finalFormat;
}
progressBar.tick(chunk.length);
Expand Down
10 changes: 5 additions & 5 deletions lib/extract.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global settings */
let childProcess = require('child_process'); // Non-const enables test mocking
const path = require('path');
const ProgressBar = require('progress');
const ProgressBar = require('../deps/progress');

const Error = require('./error');

Expand Down Expand Up @@ -40,7 +40,7 @@ function extractZipArchiveAsync(archiveFile, targetDir) {
}

return new Promise((resolve, reject) => {
let progressFormat = '\x1b[1GExtracting [:bar] :percent :etas ';
let progressFormat = '\x1b[1GExtracting [:bar] :percent-4 :eta-3s ';
let progressBar = null;
let nextData = '';
if (!settings.quiet && totalFiles > 10) {
Expand Down Expand Up @@ -77,7 +77,7 @@ function extractZipArchiveAsync(archiveFile, targetDir) {
}

if (progressBar.curr + fileCount >= totalFiles) {
let finalFormat = progressFormat.replace(/:etas/, ' ');
let finalFormat = progressFormat.replace(/:eta-3s/, ' ');
progressBar.fmt = finalFormat;
}
progressBar.tick(fileCount);
Expand Down Expand Up @@ -131,7 +131,7 @@ function extractTarArchiveAsync(archiveFile, targetDir) {
}

return new Promise((resolve, reject) => {
let progressFormat = 'Extracting [:bar] :percent :etas ';
let progressFormat = 'Extracting [:bar] :percent-4 :eta-3s ';
let progressBar = null;
if (!settings.quiet && totalFiles > 10) {
progressBar = new ProgressBar(progressFormat, {
Expand All @@ -158,7 +158,7 @@ function extractTarArchiveAsync(archiveFile, targetDir) {
if (progressBar) {
let fileCount = countChars(data.toString(), '\n');
if (progressBar.curr + fileCount >= totalFiles) {
let finalFormat = progressFormat.replace(/:etas/, ' ');
let finalFormat = progressFormat.replace(/:eta-3s/, ' ');
progressBar.fmt = finalFormat;
}
progressBar.tick(fileCount);
Expand Down
Loading

0 comments on commit 020f822

Please sign in to comment.