UNPKG

3.99 kBJavaScriptView Raw
1'use strict';
2
3const {
4 duration, formatNumber, timestamp
5} = require('./utils');
6
7const spinners = require('./spinners.json');
8
9class ProgressBar {
10 constructor({
11 total = 10, format = '[$progress]', stream = process.stderr, width,
12 complete = '=', incomplete = ' ', head = '>', clear,
13 interval, environment = {}, spinner = 'dots'
14 } = {}) {
15 this.total = total;
16 this.width = width || Math.max(this.total, 60);
17
18 this.stream = stream;
19 this.format = format;
20 this.characters = {
21 complete,
22 incomplete,
23 head
24 };
25
26 spinner = spinners[spinner] ? spinner : 'dots';
27
28 this.spinner = spinners[spinner].frames;
29 this.interval = interval || spinners[spinner].interval;
30
31 this.clear = clear === undefined ? false : clear;
32 this.environment = environment;
33 this.initialEnvironment = Object.assign({}, environment);
34
35 this.value = 0;
36
37 this.complete = false;
38 this.lastUpdate = false;
39
40 this.ticks = 0;
41 this.start = timestamp();
42 this.tick = this.start;
43 this.eta = 0;
44
45 this.render();
46 this.tick = setInterval(() => {
47 this.ticks++;
48 if (this.ticks >= this.spinner.length) {
49 this.ticks = 0;
50 }
51
52 this.render();
53 }, this.interval);
54 }
55
56 progress (increment = 1) {
57 if (this.complete) {
58 return;
59 }
60
61 this.value += increment;
62
63 this.tick = timestamp();
64 const elapsed = this.tick - this.start;
65 this.eta = elapsed / this.value * (this.total - this.value);
66
67 this.render();
68
69 if (this.value >= this.total) {
70 this.complete = true;
71 this.done();
72 }
73 }
74
75 render () {
76 if (!this.stream.isTTY) {
77 return;
78 }
79
80 let ratio = this.value / this.total;
81 ratio = Math.min(Math.max(ratio, 0), 1);
82
83 const percent = Math.floor(ratio * 100);
84
85 const now = timestamp();
86 const elapsed = now - this.start;
87 const eta = this.value === 0 ? 0 : this.eta - (now - this.tick);
88 const rate = this.value / (elapsed / 1000);
89
90 const spinner = this.spinner[this.ticks];
91
92 let string = this.format.
93 replace(/\$value/g, formatNumber(this.value)).
94 replace(/\$total/g, formatNumber(this.total)).
95 replace(/\$remaining/g, formatNumber(this.total - this.value)).
96 replace(/\$elapsed/g, duration(elapsed)).
97 replace(/\$eta/g, isNaN(eta) || !isFinite(eta) || !eta ? 'unknown' : duration(eta)).
98 replace(/\$percent/g, `${ percent.toFixed(0) }%`).
99 replace(/\$rate/g, Math.round(rate)).
100 replace(/\$spinner/g, spinner).
101 replace(/\$(.+?)\b/g, (match, token) => {
102 if (this.environment[token]) {
103 return this.environment[token];
104 }
105 if (token === 'progress') {
106 return '$progress';
107 }
108 return '';
109 });
110
111 const columns = Math.max(0, this.stream.columns - string.replace(/\$progress/g, '').length);
112 const width = Math.min(this.width, columns);
113 const completeLength = Math.round(width * ratio);
114
115 const complete = this.characters.complete.repeat(Math.max(0, completeLength)).
116 replace(/.$/, this.value >= this.total ? this.characters.complete : this.characters.head);
117 const incomplete = this.characters.incomplete.repeat(Math.max(0, width - completeLength));
118
119 string = string.replace('$progress', complete + incomplete).
120 replace(/\$progress/g, '');
121
122 if (this.lastUpdate !== string) {
123 this.stream.cursorTo(0);
124 this.stream.write(`\x1b[?25l${ string }`);
125 this.stream.clearLine(1);
126 this.lastUpdate = string;
127 }
128 }
129
130 done () {
131 clearInterval(this.tick);
132
133 this.stream.write('\x1b[?25h');
134
135 if (this.clear) {
136 if (this.stream.clearLine) {
137 this.stream.clearLine();
138 this.stream.cursorTo(0);
139 }
140 } else {
141 this.stream.write('\n');
142 }
143 }
144
145 reset () {
146 this.value = 0;
147 this.complete = 0;
148 this.start = timestamp();
149 Object.assign(this.environment, this.initialEnvironment);
150 }
151}
152
153module.exports = ProgressBar;