UNPKG

16 kBJavaScriptView Raw
1/*
2 Terminal Kit
3
4 Copyright (c) 2009 - 2020 Cédric Ronvel
5
6 The MIT License (MIT)
7
8 Permission is hereby granted, free of charge, to any person obtaining a copy
9 of this software and associated documentation files (the "Software"), to deal
10 in the Software without restriction, including without limitation the rights
11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the Software is
13 furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in all
16 copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 SOFTWARE.
25*/
26
27"use strict" ;
28
29
30
31//var string = require( 'string-kit' ) ;
32
33
34
35/*
36 progressBar( options )
37 * options `object` of options, all of them are OPTIONAL, where:
38 * width: `number` the total width of the progress bar, default to the max available width
39 * percent: `boolean` if true, it shows the progress in percent alongside with the progress bar
40 * eta: `boolean` if true, it shows the Estimated Time of Arrival alongside with the progress bar
41 * items `number` the number of items, turns the 'item mode' on
42 * title `string` the title of the current progress bar, turns the 'title mode' on
43 * barStyle `function` the style of the progress bar items, default to `term.cyan`
44 * barBracketStyle `function` the style of the progress bar bracket character, default to options.barStyle if given
45 or `term.blue`
46 * percentStyle `function` the style of percent value string, default to `term.yellow`
47 * etaStyle `function` the style of the ETA display, default to `term.bold`
48 * itemStyle `function` the style of the item display, default to `term.dim`
49 * titleStyle `function` the style of the title display, default to `term.bold`
50 * itemSize `number` the size of the item status, default to 33% of width
51 * titleSize `number` the size of the title, default to 33% of width or title.length depending on context
52 * barChar `string` the char used for the bar, default to '='
53 * barHeadChar `string` the char used for the bar, default to '>'
54 * maxRefreshTime `number` the maximum time between two refresh in ms, default to 500ms
55 * minRefreshTime `number` the minimum time between two refresh in ms, default to 100ms
56 * inline `boolean` (default: false) if true it is not locked in place, i.e. it redraws itself on the current line
57 * syncMode `boolean` true if it should work in sync mode
58 * y `integer` if set, display the progressBar on that line y-coord
59 * x `integer` if set and the 'y' option is set, display the progressBar starting on that x-coord
60*/
61module.exports = function progressBar_( options ) {
62 if ( ! options || typeof options !== 'object' ) { options = {} ; }
63
64 var controller = {} , progress , ready = false , pause = false ,
65 maxItems , itemsDone = 0 , itemsStarted = [] , itemFiller ,
66 title , titleFiller ,
67 width , y , startX , endX , oldWidth ,
68 wheel , wheelCounter = 0 , itemRollCounter = 0 ,
69 updateCount = 0 , progressUpdateCount = 0 ,
70 lastUpdateTime , lastRedrawTime ,
71 startingTime , redrawTimer ,
72 etaStartingTime , lastEta , etaFiller ;
73
74 etaStartingTime = startingTime = ( new Date() ).getTime() ;
75
76 wheel = [ '|' , '/' , '-' , '\\' ] ;
77
78 options.syncMode = !! options.syncMode ;
79
80 width = options.width || this.width - 1 ;
81
82 if ( ! options.barBracketStyle ) {
83 if ( options.barStyle ) { options.barBracketStyle = options.barStyle ; }
84 else { options.barBracketStyle = this.blue ; }
85 }
86
87 if ( ! options.barStyle ) { options.barStyle = this.cyan ; }
88 if ( ! options.percentStyle ) { options.percentStyle = this.yellow ; }
89 if ( ! options.etaStyle ) { options.etaStyle = this.bold ; }
90 if ( ! options.itemStyle ) { options.itemStyle = this.dim ; }
91 if ( ! options.titleStyle ) { options.titleStyle = this.bold ; }
92
93 if ( ! options.barChar ) { options.barChar = '=' ; }
94 else { options.barChar = options.barChar[ 0 ] ; }
95
96 if ( ! options.barHeadChar ) { options.barHeadChar = '>' ; }
97 else { options.barHeadChar = options.barHeadChar[ 0 ] ; }
98
99 if ( typeof options.maxRefreshTime !== 'number' ) { options.maxRefreshTime = 500 ; }
100 if ( typeof options.minRefreshTime !== 'number' ) { options.minRefreshTime = 100 ; }
101
102 if ( typeof options.items === 'number' ) { maxItems = options.items ; }
103 if ( maxItems && typeof options.itemSize !== 'number' ) { options.itemSize = Math.round( width / 3 ) ; }
104
105 itemFiller = ' '.repeat( options.itemSize ) ;
106
107
108 if ( options.title && typeof options.title === 'string' ) {
109 title = options.title ;
110
111 if ( typeof options.titleSize !== 'number' ) {
112 options.titleSize = Math.round( Math.min( options.title.length + 1 , width / 3 ) ) ;
113 }
114 }
115
116 titleFiller = ' '.repeat( options.titleSize ) ;
117
118
119 etaFiller = ' ' ; // 11 chars
120
121 // This is a naive ETA for instance...
122 var etaString = updated => {
123 var eta = '' , elapsedTime , elapsedEtaTime , remainingTime ,
124 averageUpdateDelay , averageUpdateProgress , lastUpdateElapsedTime , fakeProgress ;
125
126 if ( progress >= 1 ) {
127 eta = ' done' ;
128 }
129 else if ( progress > 0 ) {
130 elapsedTime = ( new Date() ).getTime() - startingTime ;
131 elapsedEtaTime = ( new Date() ).getTime() - etaStartingTime ;
132
133 if ( ! updated && progressUpdateCount > 1 ) {
134 lastUpdateElapsedTime = ( new Date() ).getTime() - lastUpdateTime ;
135 averageUpdateDelay = elapsedEtaTime / progressUpdateCount ;
136 averageUpdateProgress = progress / progressUpdateCount ;
137
138 //console.log( '\n' , elapsedEtaTime , lastUpdateElapsedTime , averageUpdateDelay , averageUpdateProgress , '\n' ) ;
139
140 // Do not update ETA if it's not an update, except if update time is too long
141 if ( lastUpdateElapsedTime < averageUpdateDelay ) {
142 fakeProgress = progress + averageUpdateProgress * lastUpdateElapsedTime / averageUpdateDelay ;
143 }
144 else {
145 fakeProgress = progress + averageUpdateProgress ;
146 }
147
148 if ( fakeProgress > 0.99 ) { fakeProgress = 0.99 ; }
149 }
150 else {
151 fakeProgress = progress ;
152 }
153
154 remainingTime = elapsedEtaTime * ( ( 1 - fakeProgress ) / fakeProgress ) / 1000 ;
155
156 eta = ' in ' ;
157
158 if ( remainingTime < 10 ) { eta += Math.round( remainingTime * 10 ) / 10 + 's' ; }
159 else if ( remainingTime < 120 ) { eta += Math.round( remainingTime ) + 's' ; }
160 else if ( remainingTime < 7200 ) { eta += Math.round( remainingTime / 60 ) + 'min' ; }
161 else if ( remainingTime < 172800 ) { eta += Math.round( remainingTime / 3600 ) + 'hours' ; }
162 else if ( remainingTime < 31536000 ) { eta += Math.round( remainingTime / 86400 ) + 'days' ; }
163 else { eta = 'few years' ; }
164 }
165 else {
166 etaStartingTime = ( new Date() ).getTime() ;
167 }
168
169 eta = ( eta + etaFiller ).slice( 0 , etaFiller.length ) ;
170 lastEta = eta ;
171
172 return eta ;
173 } ;
174
175
176
177 var redraw = updated => {
178 var time , itemIndex , itemName = itemFiller , titleName = titleFiller ,
179 innerBarSize , progressSize , voidSize ,
180 progressBar = '' , voidBar = '' , percent = '' , eta = '' ;
181
182 if ( ! ready || pause ) { return ; }
183
184 time = ( new Date() ).getTime() ;
185
186 // If progress is >= 1, then it's finished, so we should redraw NOW (before the program eventually quit)
187 if ( ( ! progress || progress < 1 ) && lastRedrawTime && time < lastRedrawTime + options.minRefreshTime ) {
188 if ( ! options.syncMode ) {
189 if ( redrawTimer ) { clearTimeout( redrawTimer ) ; }
190 redrawTimer = setTimeout( redraw.bind( this , updated ) , lastRedrawTime + options.minRefreshTime - time ) ;
191 }
192 return ;
193 }
194
195
196 this.saveCursor() ;
197
198 // If 'y' is null, we are in the blind mode, we haven't get the cursor location
199 if ( y === null ) { this.column( startX ) ; }
200 else { this.moveTo( startX , y ) ; }
201
202 //this.noFormat( Math.floor( progress * 100 ) + '%' ) ;
203
204 innerBarSize = width - 2 ;
205
206 if ( options.percent ) {
207 innerBarSize -= 4 ;
208 percent = ( ' ' + Math.round( ( progress || 0 ) * 100 ) + '%' ).slice( -4 ) ;
209 }
210
211 if ( options.eta ) {
212 eta = etaString( updated ) ;
213 innerBarSize -= eta.length ;
214 }
215
216 innerBarSize -= options.itemSize || 0 ;
217 if ( maxItems ) {
218 if ( ! itemsStarted.length ) {
219 itemName = '' ;
220 }
221 else if ( itemsStarted.length === 1 ) {
222 itemName = ' ' + itemsStarted[ 0 ] ;
223 }
224 else {
225 itemIndex = ( itemRollCounter ++ ) % itemsStarted.length ;
226 itemName = ' [' + ( itemIndex + 1 ) + '/' + itemsStarted.length + '] ' + itemsStarted[ itemIndex ] ;
227 }
228
229 if ( itemName.length > itemFiller.length ) { itemName = itemName.slice( 0 , itemFiller.length - 1 ) + '…' ; }
230 else if ( itemName.length < itemFiller.length ) { itemName = ( itemName + itemFiller ).slice( 0 , itemFiller.length ) ; }
231 }
232
233 innerBarSize -= options.titleSize || 0 ;
234 if ( title ) {
235 titleName = title ;
236
237 if ( titleName.length >= titleFiller.length ) { titleName = titleName.slice( 0 , titleFiller.length - 2 ) + '… ' ; }
238 else { titleName = ( titleName + titleFiller ).slice( 0 , titleFiller.length ) ; }
239 }
240
241 progressSize = progress === undefined ? 1 : Math.round( innerBarSize * Math.max( Math.min( progress , 1 ) , 0 ) ) ;
242 voidSize = innerBarSize - progressSize ;
243
244 /*
245 console.log( "Size:" , width ,
246 voidSize , innerBarSize , progressSize , eta.length , title.length , itemName.length ,
247 voidSize + progressSize + eta.length + title.length + itemName.length
248 ) ;
249 //*/
250
251 if ( progressSize ) {
252 if ( progress === undefined ) {
253 progressBar = wheel[ ++ wheelCounter % wheel.length ] ;
254 }
255 else {
256 progressBar += options.barChar.repeat( progressSize - 1 ) ;
257 progressBar += options.barHeadChar ;
258 }
259 }
260
261 voidBar += ' '.repeat( voidSize ) ;
262
263 options.titleStyle( titleName ) ;
264
265 if ( percent ) { options.percentStyle( percent ) ; }
266
267 if ( progress === undefined ) { this( ' ' ) ; }
268 else { options.barBracketStyle( '[' ) ; }
269
270 options.barStyle( progressBar ) ;
271 this( voidBar ) ;
272
273 if ( progress === undefined ) { this( ' ' ) ; /*this( '+' ) ;*/ }
274 else { options.barBracketStyle( ']' ) ; }
275
276 options.etaStyle( eta ) ;
277 //this( '*' ) ;
278 options.itemStyle( itemName ) ;
279 //this( '&' ) ;
280
281 this.restoreCursor() ;
282
283 if ( ! options.syncMode ) {
284 if ( redrawTimer ) { clearTimeout( redrawTimer ) ; }
285 if ( ! progress || progress < 1 ) { redrawTimer = setTimeout( redraw , options.maxRefreshTime ) ; }
286 }
287
288 lastRedrawTime = time ;
289 } ;
290
291
292 if ( options.syncMode || options.inline || options.y ) {
293 oldWidth = width ;
294
295 if ( options.y ) {
296 startX = + options.x || 1 ;
297 y = + options.y || 1 ;
298 }
299 else {
300 startX = 1 ;
301 y = null ;
302 }
303
304 endX = Math.min( startX + width , this.width ) ;
305 width = endX - startX ;
306
307 if ( width !== oldWidth ) {
308 // Should resize all part here
309 if ( options.titleSize ) { options.titleSize = Math.floor( options.titleSize * width / oldWidth ) ; }
310 if ( options.itemSize ) { options.itemSize = Math.floor( options.itemSize * width / oldWidth ) ; }
311 }
312
313 ready = true ;
314 redraw() ;
315 }
316 else {
317 // Get the cursor location before getting started
318 this.getCursorLocation( ( error , x_ , y_ ) => {
319 if ( error ) {
320 // Some bad terminals (windows...) doesn't support cursor location request, we should fallback to a decent behavior.
321 // So we just move to the last line and create a new line.
322 //cleanup( error ) ; return ;
323 this.row.eraseLineAfter( this.height )( '\n' ) ;
324 x_ = 1 ;
325 y_ = this.height ;
326 }
327
328 var oldWidth_ = width ;
329
330 startX = x_ ;
331 endX = Math.min( x_ + width , this.width ) ;
332 y = y_ ;
333 width = endX - startX ;
334
335 if ( width !== oldWidth_ ) {
336 // Should resize all part here
337 if ( options.titleSize ) { options.titleSize = Math.floor( options.titleSize * width / oldWidth_ ) ; }
338 if ( options.itemSize ) { options.itemSize = Math.floor( options.itemSize * width / oldWidth_ ) ; }
339 }
340
341 ready = true ;
342 redraw() ;
343 } ) ;
344 }
345
346 controller.startItem = name => {
347 itemsStarted.push( name ) ;
348
349 // No need to redraw NOW if there are other items running.
350 // Let the timer do the job.
351 if ( itemsStarted.length === 1 ) {
352 // If progress is >= 1, then it's finished, so we should redraw NOW (before the program eventually quit)
353 if ( progress >= 1 ) { redraw() ; return ; }
354
355 if ( options.syncMode ) {
356 redraw() ;
357 }
358 else {
359 // Using a setTimeout with a 0ms time and redrawTimer clearing has a nice effect:
360 // if multiple synchronous update are performed, redraw will be called once
361 if ( redrawTimer ) { clearTimeout( redrawTimer ) ; }
362 redrawTimer = setTimeout( redraw , 0 ) ;
363 }
364 }
365 } ;
366
367 controller.itemDone = name => {
368 var index ;
369
370 itemsDone ++ ;
371
372 if ( maxItems ) { progress = itemsDone / maxItems ; }
373 else { progress = undefined ; }
374
375 lastUpdateTime = ( new Date() ).getTime() ;
376 updateCount ++ ;
377 progressUpdateCount ++ ;
378
379 index = itemsStarted.indexOf( name ) ;
380 if ( index >= 0 ) { itemsStarted.splice( index , 1 ) ; }
381
382 // If progress is >= 1, then it's finished, so we should redraw NOW (before the program eventually quit)
383 if ( progress >= 1 ) { redraw( true ) ; return ; }
384
385 if ( options.syncMode ) {
386 redraw() ;
387 }
388 else {
389 // Using a setTimeout with a 0ms time and redrawTimer clearing has a nice effect:
390 // if multiple synchronous update are performed, redraw will be called once
391 if ( redrawTimer ) { clearTimeout( redrawTimer ) ; }
392 redrawTimer = setTimeout( redraw.bind( this , true ) , 0 ) ;
393 }
394 } ;
395
396 controller.update = toUpdate => {
397 if ( ! toUpdate ) { toUpdate = {} ; }
398 else if ( typeof toUpdate === 'number' ) { toUpdate = { progress: toUpdate } ; }
399
400 if ( 'progress' in toUpdate ) {
401 if ( typeof toUpdate.progress !== 'number' ) {
402 progress = undefined ;
403 }
404 else {
405 // Not sure if it is a good thing to let the user set progress to a value that is lesser than the current one
406 progress = toUpdate.progress ;
407
408 if ( progress > 1 ) { progress = 1 ; }
409 else if ( progress < 0 ) { progress = 0 ; }
410
411 if ( progress > 0 ) { progressUpdateCount ++ ; }
412
413 lastUpdateTime = ( new Date() ).getTime() ;
414 updateCount ++ ;
415 }
416 }
417
418 if ( typeof toUpdate.items === 'number' ) {
419 maxItems = toUpdate.items ;
420 if ( maxItems ) { progress = itemsDone / maxItems ; }
421
422 if ( typeof options.itemSize !== 'number' ) {
423 options.itemSize = Math.round( width / 3 ) ;
424 itemFiller = ' '.repeat( options.itemSize ) ;
425 }
426 }
427
428 if ( typeof toUpdate.title === 'string' ) {
429 title = toUpdate.title ;
430
431 if ( typeof options.titleSize !== 'number' ) {
432 options.titleSize = Math.round( width / 3 ) ;
433 titleFiller = ' '.repeat( options.titleSize ) ;
434 }
435 }
436
437 // If progress is >= 1, then it's finished, so we should redraw NOW (before the program eventually quit)
438 if ( progress >= 1 ) { redraw( true ) ; return ; }
439
440 if ( options.syncMode ) {
441 redraw() ;
442 }
443 else {
444 // Using a setTimeout with a 0ms time and redrawTimer clearing has a nice effect:
445 // if multiple synchronous update are performed, redraw will be called once
446 if ( redrawTimer ) { clearTimeout( redrawTimer ) ; }
447 redrawTimer = setTimeout( redraw.bind( this , true ) , 0 ) ;
448 }
449 } ;
450
451 controller.pause = controller.stop = () => {
452 pause = true ;
453 } ;
454
455 controller.resume = () => {
456 if ( pause ) {
457 pause = false ;
458 redraw() ;
459 }
460 } ;
461
462 controller.reset = () => {
463 etaStartingTime = startingTime = ( new Date() ).getTime() ;
464 itemsDone = 0 ;
465 progress = undefined ;
466 itemsStarted.length = 0 ;
467 wheelCounter = itemRollCounter = updateCount = progressUpdateCount = 0 ;
468 redraw() ;
469 } ;
470
471 return controller ;
472} ;
473