UNPKG

5.63 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, y,
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 this.clear = clear === undefined ? false : clear;
27 this.y = y || undefined;
28
29 spinner = spinners[spinner] ? spinner : 'dots';
30
31 this.spinner = spinners[spinner].frames;
32 this.interval = interval || spinners[spinner].interval;
33
34 this.environment = environment;
35 this.initialEnvironment = Object.assign({}, environment);
36
37 this.value = 0;
38
39 this.complete = false;
40 this.lastUpdate = false;
41
42 this.ticks = 0;
43 this.start = timestamp();
44 this.tick = this.start;
45 this.eta = 0;
46
47 if (this.total === 0) {
48 this.complete = true;
49 } else {
50 this.render();
51 this.ticker = setInterval(() => {
52 if (!this.complete) {
53 this.ticks++;
54 if (this.ticks >= this.spinner.length) {
55 this.ticks = 0;
56 }
57
58 this.render();
59 }
60 }, this.interval);
61 }
62 }
63
64 progress (increment = 1) {
65 if (this.complete) {
66 return;
67 }
68
69 if (increment > 0) {
70 this.value += increment;
71 } else if (increment < 0) {
72 this.total += increment;
73 }
74
75 this.tick = timestamp();
76 const elapsed = this.tick - this.start;
77 this.eta = elapsed / this.value * (this.total - this.value);
78
79 this.render();
80
81 if (this.value >= this.total) {
82 this.complete = true;
83 this.done();
84 }
85 }
86
87 render () {
88 if (!this.stream.isTTY) {
89 return;
90 }
91
92 let ratio = this.value / this.total;
93 ratio = Math.min(Math.max(ratio, 0), 1);
94
95 const percent = Math.floor(ratio * 100);
96
97 const now = timestamp();
98 const elapsed = now - this.start;
99 const eta = this.value === 0 ? 0 : this.eta - (now - this.tick);
100 const rate = this.value / (elapsed / 1000);
101
102 const spinner = this.spinner[this.ticks];
103
104 let string = this.format.
105 replace(/\$value/g, formatNumber(this.value)).
106 replace(/\$total/g, formatNumber(this.total)).
107 replace(/\$remaining/g, formatNumber(this.total - this.value)).
108 replace(/\$elapsed/g, duration(elapsed)).
109 replace(/\$eta/g, isNaN(eta) || !isFinite(eta) || !eta ? 'unknown' : duration(eta)).
110 replace(/\$percent/g, `${ percent.toFixed(0) }%`).
111 replace(/\$rate/g, Math.round(rate)).
112 replace(/\$spinner/g, spinner).
113 replace(/\$(.+?)\b/g, (match, token) => {
114 if (this.environment[token]) {
115 return this.environment[token];
116 }
117 if (token === 'progress') {
118 return '$progress';
119 }
120 return '';
121 });
122
123 const columns = Math.max(0, this.stream.columns - string.replace(/\$progress/g, '').length);
124 const width = Math.min(this.width, columns);
125
126 let completeLength = Math.max(0, Math.round(width * ratio));
127 let headLength = 0;
128 if (completeLength < this.total && completeLength > 0) {
129 completeLength--;
130 headLength = 1;
131 }
132 const incompleteLength = Math.max(0, width - (completeLength + headLength));
133
134 const head = this.characters.head.repeat(headLength);
135 const complete = this.characters.complete.repeat(completeLength);
136 const incomplete = this.characters.incomplete.repeat(incompleteLength);
137
138 string = string.replace('$progress', complete + head + incomplete).
139 replace(/\$progress/g, '');
140
141 if (this.lastUpdate !== string) {
142 this.stream.cursorTo(0, this.y);
143 this.stream.write(`\x1b[?25l${ string }`);
144 this.stream.clearLine(1);
145 this.lastUpdate = string;
146 }
147 }
148
149 done () {
150 clearInterval(this.ticker);
151
152 this.stream.write('\x1b[?25h');
153
154 if (this.clear) {
155 if (this.stream.clearLine) {
156 this.stream.clearLine();
157 this.stream.cursorTo(0, this.y);
158 }
159 } else {
160 this.stream.write('\n');
161 }
162 }
163
164 reset () {
165 this.value = 0;
166 this.complete = 0;
167 this.start = timestamp();
168 Object.assign(this.environment, this.initialEnvironment);
169 }
170}
171
172class Spinner {
173 constructor ({
174 spinner = 'dots', stream = process.stderr, x, y, interval, clear
175 } = {}) {
176 spinner = spinners[spinner] ? spinner : 'dots';
177
178 this.spinner = spinners[spinner];
179 this.interval = interval || this.spinner.interval;
180 this.frames = this.spinner.frames;
181
182 this.stream = stream;
183 this.x = x;
184 this.y = y;
185
186 this.clear = clear === undefined;
187
188 this.frame = 0;
189 }
190
191 start () {
192 if (this.update) {
193 clearInterval(this.update);
194 }
195
196 this.stream.write(' ');
197
198 this.update = setInterval(() => {
199 const character = this.frames[this.frame];
200
201 this.position();
202
203 this.stream.write(`\x1b[?25l${ character }`);
204
205 this.frame++;
206 if (this.frame >= this.frames.length) {
207 this.frame = 0;
208 }
209 }, this.interval);
210 }
211
212 position () {
213 if (this.x !== undefined && this.y !== undefined) {
214 this.stream.cursorTo(this.x, this.y);
215 } else {
216 this.stream.moveCursor(-1);
217 }
218 }
219
220 stop() {
221 clearInterval(this.update);
222
223 if (this.clear) {
224 this.position();
225 this.stream.write(' ');
226 }
227
228 this.stream.write('\x1b[?25h');
229 }
230}
231
232module.exports = {
233 ProgressBar,
234 Spinner
235};