11const proggy = require ( 'proggy' )
2- const { log, output, META } = require ( 'proc-log' )
2+ const { log, output, input , META } = require ( 'proc-log' )
33const { explain } = require ( './explain-eresolve.js' )
44const { formatWithOptions } = require ( './format' )
55
@@ -137,6 +137,9 @@ class Display {
137137 // Handlers are set immediately so they can buffer all events
138138 process . on ( 'log' , this . #logHandler)
139139 process . on ( 'output' , this . #outputHandler)
140+ process . on ( 'input' , this . #inputHandler)
141+
142+ this . #progress = new Progress ( { stream : stderr } )
140143 }
141144
142145 off ( ) {
@@ -146,9 +149,9 @@ class Display {
146149 process . off ( 'output' , this . #outputHandler)
147150 this . #outputState. buffer . length = 0
148151
149- if ( this . #progress ) {
150- this . #progress . stop ( )
151- }
152+ process . off ( 'input' , this . #inputHandler )
153+
154+ this . #progress . off ( )
152155 }
153156
154157 get chalk ( ) {
@@ -171,6 +174,7 @@ class Display {
171174 unicode,
172175 } ) {
173176 this . #command = command
177+
174178 // get createSupportsColor from chalk directly if this lands
175179 // https://github.com/chalk/chalk/pull/600
176180 const [ { Chalk } , { createSupportsColor } ] = await Promise . all ( [
@@ -201,104 +205,124 @@ class Display {
201205 // Emit resume event on the logs which will flush output
202206 log . resume ( )
203207 output . flush ( )
204- this . #startProgress( { progress, unicode } )
208+ this . #progress. load ( {
209+ unicode,
210+ enabled : ! ! progress && ! this . #silent,
211+ } )
205212 }
206213
207214 // STREAM WRITES
208215
209216 // 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 ) )
216220 }
217221
218222 // HANDLERS
219223
220224 // Arrow function assigned to a private class field so it can be passed
221225 // directly as a listener and still reference "this"
222226 #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
238245 }
239-
240- this . #tryWriteLog( level , meta , ...args )
241246 } )
242247
243248 // Arrow function assigned to a private class field so it can be passed
244249 // directly as a listener and still reference "this"
245250 #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 ) )
254267 }
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
276291 }
292+ } )
277293
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+ }
287312 }
288-
289- this . #writeOutput( level , meta , ...args )
290313 } )
291314
292315 // OUTPUT
293316
294317 #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
302326 }
303327 }
304328
@@ -344,22 +368,107 @@ class Display {
344368 this . #logColors[ level ] ( level ) ,
345369 title ? this . #logColors. title ( title ) : null ,
346370 ]
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 )
350372 }
351373 }
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+ }
352397
353- // PROGRESS
398+ load ( { enabled, unicode } ) {
399+ this . #enabled = enabled
400+ this . #spinner = unicode ? Progress . dots : Progress . lines
401+ this . #delayRender( 500 )
402+ }
354403
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) {
357414 return
358415 }
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] )
363472 }
364473}
365474
0 commit comments