1 | 'use strict';
|
2 |
|
3 | const styler = require('./style');
|
4 | const {
|
5 | duration, formatNumber, stripAnsi, timestamp
|
6 | } = require('./utils');
|
7 |
|
8 | const spinners = require('./spinners.json');
|
9 |
|
10 | class 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 |
|
197 | class 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 |
|
280 | module.exports = {
|
281 | ProgressBar,
|
282 | Spinner
|
283 | };
|