1
1
const proggy = require ( 'proggy' )
2
- const { log, output, META } = require ( 'proc-log' )
2
+ const { log, output, input , META } = require ( 'proc-log' )
3
3
const { explain } = require ( './explain-eresolve.js' )
4
4
const { formatWithOptions } = require ( './format' )
5
5
@@ -137,6 +137,9 @@ class Display {
137
137
// Handlers are set immediately so they can buffer all events
138
138
process . on ( 'log' , this . #logHandler)
139
139
process . on ( 'output' , this . #outputHandler)
140
+ process . on ( 'input' , this . #inputHandler)
141
+
142
+ this . #progress = new Progress ( { stream : stderr } )
140
143
}
141
144
142
145
off ( ) {
@@ -146,9 +149,9 @@ class Display {
146
149
process . off ( 'output' , this . #outputHandler)
147
150
this . #outputState. buffer . length = 0
148
151
149
- if ( this . #progress ) {
150
- this . #progress . stop ( )
151
- }
152
+ process . off ( 'input' , this . #inputHandler )
153
+
154
+ this . #progress . off ( )
152
155
}
153
156
154
157
get chalk ( ) {
@@ -171,6 +174,7 @@ class Display {
171
174
unicode,
172
175
} ) {
173
176
this . #command = command
177
+
174
178
// get createSupportsColor from chalk directly if this lands
175
179
// https://github.com/chalk/chalk/pull/600
176
180
const [ { Chalk } , { createSupportsColor } ] = await Promise . all ( [
@@ -201,104 +205,124 @@ class Display {
201
205
// Emit resume event on the logs which will flush output
202
206
log . resume ( )
203
207
output . flush ( )
204
- this . #startProgress( { progress, unicode } )
208
+ this . #progress. load ( {
209
+ unicode,
210
+ enabled : ! ! progress && ! this . #silent,
211
+ } )
205
212
}
206
213
207
214
// STREAM WRITES
208
215
209
216
// Write formatted and (non-)colorized output to streams
210
- #stdoutWrite ( options , ...args ) {
211
- this . #stdout. write ( formatWithOptions ( { colors : this . #stdoutColor, ...options } , ...args ) )
212
- }
213
-
214
- #stderrWrite ( options , ...args ) {
215
- this . #stderr. write ( formatWithOptions ( { colors : this . #stderrColor, ...options } , ...args ) )
217
+ #write ( stream , options , ...args ) {
218
+ const colors = stream === this . #stdout ? this . #stdoutColor : this . #stderrColor
219
+ this . #progress. write ( stream , formatWithOptions ( { colors, ...options } , ...args ) )
216
220
}
217
221
218
222
// HANDLERS
219
223
220
224
// Arrow function assigned to a private class field so it can be passed
221
225
// directly as a listener and still reference "this"
222
226
#logHandler = withMeta ( ( level , meta , ...args ) => {
223
- if ( level === log . KEYS . resume ) {
224
- this . #logState. buffering = false
225
- this . #logState. buffer . forEach ( ( item ) => this . #tryWriteLog( ...item ) )
226
- this . #logState. buffer . length = 0
227
- return
228
- }
229
-
230
- if ( level === log . KEYS . pause ) {
231
- this . #logState. buffering = true
232
- return
233
- }
234
-
235
- if ( this . #logState. buffering ) {
236
- this . #logState. buffer . push ( [ level , meta , ...args ] )
237
- return
227
+ switch ( level ) {
228
+ case log . KEYS . resume :
229
+ this . #logState. buffering = false
230
+ this . #logState. buffer . forEach ( ( item ) => this . #tryWriteLog( ...item ) )
231
+ this . #logState. buffer . length = 0
232
+ break
233
+
234
+ case log . KEYS . pause :
235
+ this . #logState. buffering = true
236
+ break
237
+
238
+ default :
239
+ if ( this . #logState. buffering ) {
240
+ this . #logState. buffer . push ( [ level , meta , ...args ] )
241
+ } else {
242
+ this . #tryWriteLog( level , meta , ...args )
243
+ }
244
+ break
238
245
}
239
-
240
- this . #tryWriteLog( level , meta , ...args )
241
246
} )
242
247
243
248
// Arrow function assigned to a private class field so it can be passed
244
249
// directly as a listener and still reference "this"
245
250
#outputHandler = withMeta ( ( level , meta , ...args ) => {
246
- if ( level === output . KEYS . flush ) {
247
- this . #outputState. buffering = false
248
-
249
- if ( meta . jsonError && this . #json) {
250
- const json = { }
251
- for ( const item of this . #outputState. buffer ) {
252
- // index 2 skips the level and meta
253
- Object . assign ( json , tryJsonParse ( item [ 2 ] ) )
251
+ switch ( level ) {
252
+ case output . KEYS . flush :
253
+ this . #outputState. buffering = false
254
+ if ( meta . jsonError && this . #json) {
255
+ const json = { }
256
+ for ( const item of this . #outputState. buffer ) {
257
+ // index 2 skips the level and meta
258
+ Object . assign ( json , tryJsonParse ( item [ 2 ] ) )
259
+ }
260
+ this . #writeOutput(
261
+ output . KEYS . standard ,
262
+ meta ,
263
+ JSON . stringify ( { ...json , error : meta . jsonError } , null , 2 )
264
+ )
265
+ } else {
266
+ this . #outputState. buffer . forEach ( ( item ) => this . #writeOutput( ...item ) )
254
267
}
255
- this . #writeOutput(
256
- output . KEYS . standard ,
257
- meta ,
258
- JSON . stringify ( { ...json , error : meta . jsonError } , null , 2 )
259
- )
260
- } else {
261
- this . #outputState. buffer . forEach ( ( item ) => this . #writeOutput( ...item ) )
262
- }
263
-
264
- this . #outputState. buffer . length = 0
265
- return
266
- }
267
-
268
- if ( level === output . KEYS . buffer ) {
269
- this . #outputState. buffer . push ( [ output . KEYS . standard , meta , ...args ] )
270
- return
271
- }
272
-
273
- if ( this . #outputState. buffering ) {
274
- this . #outputState. buffer . push ( [ level , meta , ...args ] )
275
- return
268
+ this . #outputState. buffer . length = 0
269
+ break
270
+
271
+ case output . KEYS . buffer :
272
+ this . #outputState. buffer . push ( [ output . KEYS . standard , meta , ...args ] )
273
+ break
274
+
275
+ default :
276
+ if ( this . #outputState. buffering ) {
277
+ this . #outputState. buffer . push ( [ level , meta , ...args ] )
278
+ } else {
279
+ // HACK: if it looks like the banner and we are in a state where we hide the
280
+ // banner then dont write any output. This hack can be replaced with proc-log.META
281
+ const isBanner = args . length === 1 &&
282
+ typeof args [ 0 ] === 'string' &&
283
+ args [ 0 ] . startsWith ( '\n> ' ) &&
284
+ args [ 0 ] . endsWith ( '\n' )
285
+ const hideBanner = this . #silent || [ 'exec' , 'explore' ] . includes ( this . #command)
286
+ if ( ! ( isBanner && hideBanner ) ) {
287
+ this . #writeOutput( level , meta , ...args )
288
+ }
289
+ }
290
+ break
276
291
}
292
+ } )
277
293
278
- // HACK: if it looks like the banner and we are in a state where we hide the
279
- // banner then dont write any output. This hack can be replaced with proc-log.META
280
- const isBanner = args . length === 1 &&
281
- typeof args [ 0 ] === 'string' &&
282
- args [ 0 ] . startsWith ( '\n> ' ) &&
283
- args [ 0 ] . endsWith ( '\n' )
284
- const hideBanner = this . #silent || [ 'exec' , 'explore' ] . includes ( this . #command)
285
- if ( isBanner && hideBanner ) {
286
- return
294
+ #inputHandler = withMeta ( ( level , meta , ...args ) => {
295
+ switch ( level ) {
296
+ case input . KEYS . start :
297
+ log . pause ( )
298
+ this . #outputState. buffering = true
299
+ this . #progress. pause ( )
300
+ break
301
+
302
+ case input . KEYS . end :
303
+ log . resume ( )
304
+ output . flush ( )
305
+ this . #progress. resume ( )
306
+ break
307
+
308
+ case input . KEYS . read : {
309
+ const [ res , rej , p ] = args
310
+ return input . start ( ( ) => p ( ) . then ( res ) . catch ( rej ) . finally ( ( ) => output . standard ( '' ) ) )
311
+ }
287
312
}
288
-
289
- this . #writeOutput( level , meta , ...args )
290
313
} )
291
314
292
315
// OUTPUT
293
316
294
317
#writeOutput ( level , meta , ...args ) {
295
- if ( level === output . KEYS . standard ) {
296
- this . #stdoutWrite( { } , ...args )
297
- return
298
- }
299
-
300
- if ( level === output . KEYS . error ) {
301
- this . #stderrWrite( { } , ...args )
318
+ switch ( level ) {
319
+ case output . KEYS . standard :
320
+ this . #write( this . #stdout, { } , ...args )
321
+ break
322
+
323
+ case output . KEYS . error :
324
+ this . #write( this . #stderr, { } , ...args )
325
+ break
302
326
}
303
327
}
304
328
@@ -344,22 +368,107 @@ class Display {
344
368
this . #logColors[ level ] ( level ) ,
345
369
title ? this . #logColors. title ( title ) : null ,
346
370
]
347
- this . #stderrWrite( { prefix } , ...args )
348
- } else if ( this . #progress) {
349
- // TODO: make this display a single log line of filtered messages
371
+ this . #write( this . #stderr, { prefix } , ...args )
350
372
}
351
373
}
374
+ }
375
+
376
+ class Progress {
377
+ // Taken from https://github.com/sindresorhus/cli-spinners
378
+ // MIT License
379
+ // Copyright (c) Sindre Sorhus <[email protected] > (https://sindresorhus.com)
380
+ static dots = { duration : 80 , frames : [ '⠋' , '⠙' , '⠹' , '⠸' , '⠼' , '⠴' , '⠦' , '⠧' , '⠇' , '⠏' ] }
381
+ static lines = { duration : 130 , frames : [ '-' , '\\' , '|' , '/' ] }
382
+
383
+ #stream
384
+ #spinner
385
+ #client
386
+ #enabled = false
387
+
388
+ #frameIndex = 0
389
+ #lastUpdate = 0
390
+ #interval
391
+ #initialTimeout
392
+
393
+ constructor ( { stream } ) {
394
+ this . #client = proggy . createClient ( { normalize : true } )
395
+ this . #stream = stream
396
+ }
352
397
353
- // PROGRESS
398
+ load ( { enabled, unicode } ) {
399
+ this . #enabled = enabled
400
+ this . #spinner = unicode ? Progress . dots : Progress . lines
401
+ this . #delayRender( 500 )
402
+ }
354
403
355
- #startProgress ( { progress, unicode } ) {
356
- if ( ! progress || this . #silent) {
404
+ off ( ) {
405
+ this . #clear( )
406
+ }
407
+
408
+ pause ( ) {
409
+ this . #clear( { clearLine : true } )
410
+ }
411
+
412
+ #clear ( { clearLine } = { } ) {
413
+ if ( ! this . #enabled) {
357
414
return
358
415
}
359
- this . #progress = proggy . createClient ( { normalize : true } )
360
- // TODO: implement proggy trackers in arborist/doctor
361
- // TODO: listen to progress events here and build progress UI
362
- // TODO: see deprecated gauge package for what unicode chars were used
416
+ clearTimeout ( this . #initialTimeout)
417
+ this . #initialTimeout = null
418
+ clearInterval ( this . #interval)
419
+ this . #interval = null
420
+ this . #frameIndex = 0
421
+ this . #lastUpdate = 0
422
+ this . #stream. cursorTo ( 0 )
423
+ if ( clearLine ) {
424
+ this . #stream. clearLine ( 1 )
425
+ }
426
+ }
427
+
428
+ resume ( ) {
429
+ this . #delayRender( 10 )
430
+ }
431
+
432
+ write ( stream , str ) {
433
+ if ( ! this . #enabled || ! this . #interval) {
434
+ return stream . write ( str )
435
+ }
436
+ this . #stream. cursorTo ( 0 )
437
+ if ( str . startsWith ( '\n' ) ) {
438
+ this . #stream. write ( ' ' )
439
+ this . #stream. cursorTo ( 0 )
440
+ }
441
+ stream . write ( str )
442
+ this . #render( )
443
+ }
444
+
445
+ #delayRender ( ms ) {
446
+ this . #initialTimeout = setTimeout ( ( ) => {
447
+ this . #initialTimeout = null
448
+ this . #render( )
449
+ } , ms )
450
+ this . #initialTimeout. unref ( )
451
+ }
452
+
453
+ #render ( ) {
454
+ if ( ! this . #enabled || this . #initialTimeout) {
455
+ return
456
+ }
457
+ this . #renderFrame( Date . now ( ) - this . #lastUpdate >= this . #spinner. duration )
458
+ clearInterval ( this . #interval)
459
+ this . #interval = setInterval ( ( ) => this . #renderFrame( true ) , this . #spinner. duration )
460
+ }
461
+
462
+ #renderFrame ( next ) {
463
+ if ( next ) {
464
+ this . #lastUpdate = Date . now ( )
465
+ this . #frameIndex++
466
+ if ( this . #frameIndex >= this . #spinner. frames . length ) {
467
+ this . #frameIndex = 0
468
+ }
469
+ }
470
+ this . #stream. cursorTo ( 0 )
471
+ this . #stream. write ( this . #spinner. frames [ this . #frameIndex] )
363
472
}
364
473
}
365
474
0 commit comments