UNPKG

6.73 kBJavaScriptView Raw
1var _readline = require('readline');
2
3// Progress-Bar constructor
4function Bar(options){
5 // options set ?
6 options = options || {};
7
8 // the update timer
9 this.timer = null;
10
11 // the current bar value
12 this.value = 0;
13
14 // the end value of the bar
15 this.total = 100;
16
17 // the max update rate in fps (redraw will only triggered on value change)
18 this.throttleTime = 1000 / (options.fps || 10);
19
20 // the output stream to write on
21 this.stream = options.stream || process.stderr;
22
23 // clear on finish ?
24 this.clearOnComplete = options.clearOnComplete || false;
25
26 // last drawn string - only render on change!
27 this.lastDrawnString = null;
28
29 // size of the progressbar in chars
30 this.barsize = options.barsize || 40;
31
32 // hide the cursor ?
33 this.hideCursor = options.hideCursor || false;
34
35 // pre-render bar strings (performance)
36 this.barCompleteString = (new Array(this.barsize + 1 ).join(options.barCompleteChar || '='));
37 this.barIncompleteString = (new Array(this.barsize + 1 ).join(options.barIncompleteChar || '-'));
38
39 // the bar format
40 this.format = options.format || 'progress [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}';
41
42 // start time (used for eta calculation)
43 this.startTime = null;
44
45 // last update time
46 this.lastRedraw = Date.now();
47
48 // the number of results to average ETA over
49 this.etaBuffer = options.etaBuffer || 10;
50
51 // eta buffer
52 this.eta = {};
53};
54
55// internal render function
56Bar.prototype.render = function(){
57 this.stopTimer();
58
59 // copy format string
60 var s = this.format;
61
62 // calculate the normalized current progress
63 var progress = this.value/this.total;
64
65 // limiter
66 progress = Math.min(Math.max(progress, 0.0), 1.0);
67
68 // generate bar string by stripping the pre-rendered strings
69 var b = this.barCompleteString.substr(0, Math.round(progress*this.barsize)) +
70 this.barIncompleteString.substr(0, Math.round((1.0-progress)*this.barsize));
71
72 // limit the bar-size (can cause n+1 chars in some numerical situation)
73 b = b.substr(0, this.barsize);
74
75 // calculate progress in percent
76 var percentage = Math.round(progress*100) + '';
77
78 // calculate elapsed time
79 var elapsedTime = Math.round((Date.now() - this.startTime)/1000);
80 var elapsedTimef = this.formatTime(elapsedTime);
81
82 // calculate eta
83 var eta = this.eta.time;
84 var etaf = this.formatTime(eta, 5);
85
86 // assign placeholder tokens
87 s = s.replace(/\{bar}/gi, b)
88 .replace(/\{percentage}/gi, percentage)
89 .replace(/\{total}/gi, this.total + '')
90 .replace(/\{value}/gi, this.value + '')
91 .replace(/\{eta}/gi, eta + '')
92 .replace(/\{eta_formatted}/gi, etaf + '')
93 .replace(/\{duration}/gi, elapsedTime + '')
94 .replace(/\{duration_formatted}/gi, elapsedTimef + '');
95
96 // string changed ? only trigger redraw on change!
97 if (this.lastDrawnString != s){
98 // set cursor to start of line
99 _readline.cursorTo(this.stream, 0);
100
101 // write output
102 this.stream.write(s);
103
104 // clear to the right from cursor
105 _readline.clearLine(this.stream, 1);
106
107 // store string
108 this.lastDrawnString = s;
109
110 // set last redraw time
111 this.lastRedraw = Date.now();
112 }
113
114 // next update
115 this.timer = setTimeout(this.render.bind(this), this.throttleTime*2);
116};
117
118// format a number of seconds into hours and minutes as appropriate
119Bar.prototype.formatTime = function (t, roundTo) {
120 var round = function (input) {
121 if (roundTo) {
122 return roundTo * Math.round(input / roundTo);
123 } else {
124 return input
125 }
126 }
127 if (t > 3600) {
128 return Math.floor(t / 3600) + 'h' + round((t % 3600) / 60) + 'm';
129 } else if (t > 60) {
130 return Math.floor(t / 60) + 'm' + round((t % 60)) + 's';
131 } else if (t > 10) {
132 return round(t) + 's';
133 } else {
134 return t + 's';
135 }
136}
137
138// start the progress bar
139Bar.prototype.start = function(total, startValue){
140 // progress is only visible in TTY mode!
141 if (!this.stream.isTTY){
142 return;
143 }
144
145 // set initial values
146 this.value = startValue || 0;
147 this.total = total || 100;
148 this.startTime = Date.now();
149 this.lastDrawnString = '';
150
151 // hide the cursor
152 if (this.hideCursor) {
153 this.stream.write('\033[?25l');
154 }
155
156 // timer already active ?
157 this.stopTimer();
158
159 // redraw on start!
160 this.render();
161
162 // initialize eta buffer
163 this.eta = {
164 valueBuffer: [this.value],
165 timeBuffer: [this.startTime],
166 time: null
167 };
168};
169
170// stop the bar
171Bar.prototype.stop = function(){
172 // timer inactive ?
173 if (!this.timer) {
174 return
175 }
176
177 // trigger final rendering
178 this.render();
179 this.stopTimer();
180
181 // clear line on complete ?
182 if (this.clearOnComplete && this.stream.isTTY){
183 _readline.cursorTo(this.stream, 0);
184 _readline.clearLine(this.stream, 0);
185 }else{
186 // new line on complete
187 this.stream.write('\n');
188 }
189
190 // show the cursor
191 if (this.hideCursor) {
192 this.stream.write('\033[?25h');
193 }
194};
195
196// update the bar value
197Bar.prototype.update = function(current){
198 // update value
199 this.value = current;
200
201 this.eta.valueBuffer.push(current);
202 this.eta.timeBuffer.push(Date.now());
203
204 // throttle the update or force update ?
205 if (this.lastRedraw + this.throttleTime < Date.now()){
206 this.calculateETA();
207 this.render();
208 }
209};
210
211// update the bar value
212Bar.prototype.increment = function(step){
213 step = step || 1;
214 this.update(this.value + step);
215};
216
217// get the total (limit) value
218Bar.prototype.getTotal = function(){
219 return this.total;
220};
221
222// internal - stop the current timer
223Bar.prototype.stopTimer = function(){
224 // stop the timer
225 if (this.timer) {
226 clearTimeout(this.timer);
227 }
228 this.timer = null;
229};
230
231// internal - eta calculation
232Bar.prototype.calculateETA = function(){
233
234 var l = this.eta.valueBuffer.length;
235 var buffer = Math.min(this.etaBuffer, l);
236
237 var v_diff = this.eta.valueBuffer[l - 1] - this.eta.valueBuffer[l - buffer];
238 var t_diff = this.eta.timeBuffer[l - 1] - this.eta.timeBuffer[l - buffer];
239
240 // get progress per ms
241 var vt_rate = v_diff/t_diff;
242
243 // remaining
244 var remaining = this.total-this.value;
245
246 // eq: vt_rate *x = total
247 var eta = remaining/vt_rate/1000;
248
249 this.eta = {
250 valueBuffer: this.eta.valueBuffer.slice(-this.etaBuffer),
251 timeBuffer: this.eta.timeBuffer.slice(-this.etaBuffer),
252 time: Math.ceil(eta)
253 }
254};
255
256module.exports = Bar;