UNPKG

7 kBJavaScriptView Raw
1'use strict';
2
3const styler = require('./style');
4const {
5 duration, formatNumber, stripAnsi, timestamp
6} = require('./utils');
7
8const spinners = require('./spinners.json');
9
10class ProgressBar {
11 constructor ({
12 total = 10, value = 0, format = '[$progress]', stream = process.stderr,
13 y, x = 0, width, complete = '=', incomplete = ' ', head = '>', clear,
14 interval, tokens = {}, spinner = 'dots', spinnerStyle,
15 durationOptions, formatOptions
16 } = {}) {
17 this._total = total;
18 this._value = value;
19
20 this.width = width || Math.max(this._total, 60);
21
22 this.stream = stream;
23 this.format = format;
24 this.characters = {
25 complete,
26 incomplete,
27 head
28 };
29
30 this.clear = clear === undefined ? false : clear;
31 this.y = y || undefined;
32 this.x = x;
33
34 spinner = spinners[spinner] ? spinner : 'dots';
35
36 this.spinner = spinners[spinner].frames;
37 this.interval = interval || spinners[spinner].interval;
38 this.spinnerStyle = spinnerStyle;
39
40 this.durationOptions = durationOptions;
41 this.formatOptions = formatOptions;
42
43 this.tokens = tokens;
44 this.initialTokens = Object.assign({}, tokens);
45
46 this.complete = false;
47 this.lastUpdate = false;
48
49 this.ticks = 0;
50 this.start = timestamp();
51 this.tick = this.start;
52 this.eta = 0;
53
54 if (this._total === 0) {
55 this.complete = true;
56 } else {
57 this.render();
58 this.ticker = setInterval(() => {
59 if (!this.complete) {
60 this.ticks++;
61 if (this.ticks >= this.spinner.length) {
62 this.ticks = 0;
63 }
64
65 this.render();
66 }
67 }, this.interval);
68 }
69 }
70
71 get value () {
72 return this._value;
73 }
74
75 set value (value) {
76 if (this.complete) {
77 return;
78 }
79
80 this._value = value;
81
82 this.tick = timestamp();
83 const elapsed = this.tick - this.start;
84 this.eta = elapsed / this._value * (this._total - this._value);
85
86 this.render();
87
88 if (this._value >= this._total) {
89 this.done();
90 }
91 }
92
93 get total () {
94 return this._total;
95 }
96
97 set total (value) {
98 this._total = value;
99 this.render();
100 }
101
102 render () {
103 if (!this.stream.isTTY) {
104 return;
105 }
106
107 let ratio = this._value / this._total;
108 ratio = Math.min(Math.max(ratio, 0), 1);
109
110 const percent = Math.floor(ratio * 100);
111
112 const now = timestamp();
113 const elapsed = now - this.start;
114 const eta = this._value === 0 ? 0 : this.eta - (now - this.tick);
115 const rate = this._value / (elapsed / 1000);
116
117 const spinner = this.spinnerStyle ? styler(this.spinner[this.ticks], this.spinnerStyle) :
118 this.spinner[this.ticks];
119
120 let string = this.format.
121 replace(/\$value/g, formatNumber(this._value, this.formatOptions)).
122 replace(/\$total/g, formatNumber(this._total, this.formatOptions)).
123 replace(/\$remaining/g, formatNumber(this._total - this._value, this.formatOptions)).
124 replace(/\$elapsed/g, duration(elapsed, this.durationOptions)).
125 replace(/\$eta/g, isNaN(eta) || !isFinite(eta) || !eta ? 'unknown' : duration(eta, this.durationOptions)).
126 replace(/\$percent/g, `${ percent.toFixed(0) }%`).
127 replace(/\$rate/g, Math.round(rate)).
128 replace(/\$spinner/g, spinner).
129 replace(/\$(.+?)\b/g, (match, token) => {
130 if (this.tokens[token] !== undefined) {
131 let value = this.tokens[token];
132
133 if (typeof value === 'number') {
134 value = formatNumber(value, this.formatOptions);
135 }
136
137 return value;
138 }
139 if (token === 'progress') {
140 return '$progress';
141 }
142 return '';
143 });
144
145 const length = stripAnsi(string.replace(/\$progress/g, '')).length;
146 const columns = Math.max(0, this.stream.columns - length);
147 const width = Math.min(this.width, columns);
148
149 let completeLength = Math.max(0, Math.round(width * ratio));
150 let headLength = 0;
151 if (this._value < this._total && completeLength > 0 && this.characters.head) {
152 headLength = 1;
153 completeLength--;
154 }
155 const incompleteLength = Math.max(0, width - (completeLength + headLength));
156
157 const head = headLength ? this.characters.head : '';
158 const complete = this.characters.complete.repeat(completeLength);
159 const incomplete = this.characters.incomplete.repeat(incompleteLength);
160
161 string = string.replace('$progress', complete + head + incomplete).
162 replace(/\$progress/g, '');
163
164 if (this.lastUpdate !== string) {
165 this.stream.cursorTo(this.x, this.y);
166 this.stream.write(`\x1b[?25l${ string }`);
167 this.stream.clearLine(1);
168 this.lastUpdate = string;
169 }
170 }
171
172 done () {
173 this.complete = true;
174
175 clearInterval(this.ticker);
176
177 if (this.clear) {
178 if (this.stream.clearLine) {
179 this.stream.cursorTo(this.x, this.y);
180 this.stream.clearLine(1);
181 }
182 } else {
183 this.stream.write('\n');
184 }
185
186 this.stream.write('\x1b[?25h');
187 }
188
189 reset () {
190 this._value = 0;
191 this.complete = false;
192 this.start = timestamp();
193 Object.assign(this.tokens, this.initialTokens);
194 }
195}
196
197class Spinner {
198 constructor ({
199 spinner = 'dots', stream = process.stderr, x, y, interval, clear,
200 style, prepend = '', append = ''
201 } = {}) {
202 spinner = spinners[spinner] ? spinner : 'dots';
203
204 this.spinner = spinners[spinner];
205 this.interval = interval || this.spinner.interval;
206 this.frames = this.spinner.frames;
207
208 this.stream = stream;
209 this.x = x;
210 this.y = y;
211
212 if (this.y !== undefined && this.x === undefined) {
213 this.x = 0;
214 }
215
216 this.style = style;
217 this.prepend = prepend;
218 this.append = append;
219
220 this.offset = stripAnsi(this.prepend).length;
221
222 this.clear = clear === undefined;
223
224 this.frame = 0;
225 }
226
227 start () {
228 if (this.update) {
229 clearInterval(this.update);
230 }
231
232 this.stream.write('\x1b[?25l');
233
234 if (this.x !== undefined) {
235 this.stream.cursorTo(this.x, this.y);
236 }
237 this.stream.write(`${ this.prepend } ${ this.append }`);
238 this.stream.moveCursor(this.append.length * -1);
239
240 this.update = setInterval(() => {
241 const character = this.style ? styler(this.frames[this.frame], this.style) :
242 this.frames[this.frame];
243
244 this.position();
245
246 this.stream.write(`\x1b[?25l${ character }`);
247
248 this.frame++;
249 if (this.frame >= this.frames.length) {
250 this.frame = 0;
251 }
252 }, this.interval);
253 }
254
255 position () {
256 if (this.x !== undefined) {
257 this.stream.cursorTo(this.x + this.offset, this.y);
258 } else {
259 this.stream.moveCursor(-1);
260 }
261 }
262
263 stop () {
264 clearInterval(this.update);
265
266 if (this.clear) {
267 if (this.x !== undefined) {
268 this.stream.cursorTo(this.x, this.y);
269 } else {
270 this.position();
271 this.stream.moveCursor(this.offset * -1);
272 }
273 this.stream.clearLine(1);
274 }
275
276 this.stream.write('\x1b[?25h');
277 }
278}
279
280module.exports = {
281 ProgressBar,
282 Spinner
283};