import cliTruncate from 'cli-truncate' ; import logUpdate from 'log-update' ; import { EOL } from 'os' ; import cliWrap from 'wrap-ansi' ; import { ListrContext } from '@interfaces/listr.interface' ; import { ListrRenderer } from '@interfaces/renderer.interface' ; import { Task } from '@lib/task' ; import colorette from '@utils/colorette' ; import { figures } from '@utils/figures' ; import { indentString } from '@utils/indent-string' ; import { isUnicodeSupported } from '@utils/is-unicode-supported' ; import { parseTaskTime } from '@utils/parse-time' ; /** Default updating renderer for Listr2 */ export class DefaultRenderer implements ListrRenderer { /** designates whether this renderer can output to a non-tty console */ public static nonTTY = false ; /** renderer options for the defauult renderer */ public static rendererOptions : { default indentation ? : number ; default global clearOutput ? : boolean ; default showSubtasks ? : boolean ; default collapse ? : boolean ; default showSkipMessage ? : boolean ; default collapseSkips ? : boolean ; default suffixSkips ? : boolean ; default showErrorMessage ? : boolean ; default collapseErrors ? : boolean ; default suffixRetries ? : boolean ; default global lazy ? : boolean ; default global showTimer ? : boolean ; default removeEmptyLines ? : boolean ; default global formatOutput ? : 'truncate' | 'wrap' ; } = { indentation : 2 , clearOutput : false , showSubtasks : true , collapse : true , collapseSkips : true , showSkipMessage : true , suffixSkips : true , collapseErrors : true , showErrorMessage : true , suffixRetries : true , lazy : false , showTimer : false , removeEmptyLines : true , formatOutput : 'truncate' } ; /** per task options for the default renderer */ public static rendererTaskOptions : { default bottomBar ? : boolean | number ; default persistentOutput ? : boolean ; /**
         * show the task time if it was successful
         */ showTimer ? : boolean ; } ; private id ? : NodeJS . Timeout ; private bottomBar : { [ uuid ] : { data ? : string [ ] ; items ? : number ; } ; } = { } ; private promptBar : string ; private readonly spinner : string [ ] = ! isUnicodeSupported ( ) ? [ '-' , '\\' , '|' , '/' ] : [ '⠋' , '⠙' , '⠹' , '⠸' , '⠼' , '⠴' , '⠦' , '⠧' , '⠇' , '⠏' ] ; private spinnerPosition = 0 ; constructor ( public tasks , public options , public renderHook$ ? ) { this . options = { ... DefaultRenderer . rendererOptions , ... this . options } ; } public getTaskOptions ( task ) { return { ... DefaultRenderer . rendererTaskOptions , ... task . rendererTaskOptions } ; } public isBottomBar ( task ) { const bottomBar = this . getTaskOptions ( task ) . bottomBar ; return typeof bottomBar === 'number' && bottomBar !== 0 || typeof bottomBar === 'boolean' && bottomBar !== false ; } public hasPersistentOutput ( task ) { return this . getTaskOptions ( task ) . persistentOutput === true ; } public hasTimer ( task ) { return this . getTaskOptions ( task ) . showTimer === true ; } public getSelfOrParentOption < T extends keyof typeof DefaultRenderer [ 'rendererOptions' ] > ( task , key ) { return task ?. rendererOptions ?. [ key ] ?? this . options ?. [ key ] ; } public getTaskTime ( task ) { return colorette . dim ( `[${ parseTaskTime ( task . message . duration ) }]` ) ; } public createRender ( options ? ) { options = { ... { tasks : true , bottomBar : true , prompt : true } , ... options } ; const render = [ ] ; const renderTasks = this . multiLineRenderer ( this . tasks ) ; const renderBottomBar = this . renderBottomBar ( ) ; const renderPrompt = this . renderPrompt ( ) ; if ( options . tasks && renderTasks ?. trim ( ) . length > 0 ) { render . push ( renderTasks ) ; } if ( options . bottomBar && renderBottomBar ?. trim ( ) . length > 0 ) { render . push ( ( render . length > 0 ? EOL : '' ) + renderBottomBar ) ; } if ( options . prompt && renderPrompt ?. trim ( ) . length > 0 ) { render . push ( ( render . length > 0 ? EOL : '' ) + renderPrompt ) ; } return render . length > 0 ? render . join ( EOL ) : '' ; } public render ( ) { if ( this . id ) { return ; } const updateRender = ( ) : void => logUpdate ( this . createRender ( ) ) ; if ( ! this . options ?. lazy ) { this . id = setInterval ( ( ) => { this . spinnerPosition = ++ this . spinnerPosition % this . spinner . length ; updateRender ( ) ; } , 100 ) ; } this . renderHook$ . subscribe ( ( ) => { updateRender ( ) ; } ) ; } public end ( ) { clearInterval ( this . id ) ; if ( this . id ) { this . id = undefined ; } logUpdate . clear ( ) ; logUpdate . done ( ) ; if ( ! this . options . clearOutput ) { process . stdout . write ( this . createRender ( { prompt : false } ) + EOL ) ; } } private multiLineRenderer ( tasks , level = 0 ) { let output = [ ] ; for ( const task of tasks ) { if ( task . isEnabled ( ) ) { if ( task . hasTitle ( ) ) { if ( ! ( tasks . some ( ( task ) => task . hasFailed ( ) ) && ! task . hasFailed ( ) && task . options . exitOnError !== false && ! ( task . isCompleted ( ) || task . isSkipped ( ) ) ) ) { if ( task . hasFailed ( ) && this . getSelfOrParentOption ( task , 'collapseErrors' ) ) { output = [ ... output , this . formatString ( ! task . hasSubtasks ( ) && task . message . error && this . getSelfOrParentOption ( task , 'showErrorMessage' ) ? task . message . error : task . title , this . getSymbol ( task ) , level ) ] ; } else if ( task . isSkipped ( ) && this . getSelfOrParentOption ( task , 'collapseSkips' ) ) { output = [ ... output , this . formatString ( this . addSuffixToMessage ( task . message . skip && this . getSelfOrParentOption ( task , 'showSkipMessage' ) ? task . message . skip : task . title , 'SKIPPED' , this . getSelfOrParentOption ( task , 'suffixSkips' ) ) , this . getSymbol ( task ) , level ) ] ; } else if ( task . isRetrying ( ) && this . getSelfOrParentOption ( task , 'suffixRetries' ) ) { output = [ ... output , this . formatString ( this . addSuffixToMessage ( task . title , `RETRYING-${ task . message . retry . count }` ) , this . getSymbol ( task ) , level ) ] ; } else if ( task . isCompleted ( ) && task . hasTitle ( ) && ( this . getSelfOrParentOption ( task , 'showTimer' ) || this . hasTimer ( task ) ) ) { output = [ ... output , this . formatString ( `${ task ?. title } ${ this . getTaskTime ( task ) }` , this . getSymbol ( task ) , level ) ] ; } else { output = [ ... output , this . formatString ( task . title , this . getSymbol ( task ) , level ) ] ; } } else { output = [ ... output , this . formatString ( task . title , colorette . red ( figures . squareSmallFilled ) , level ) ] ; } } if ( ! task . hasSubtasks ( ) || ! this . getSelfOrParentOption ( task , 'showSubtasks' ) ) { if ( task . hasFailed ( ) && this . getSelfOrParentOption ( task , 'collapseErrors' ) === false && ( this . getSelfOrParentOption ( task , 'showErrorMessage' ) || ! this . getSelfOrParentOption ( task , 'showSubtasks' ) ) ) { output = [ ... output , this . dumpData ( task , level , 'error' ) ] ; } else if ( task . isSkipped ( ) && this . getSelfOrParentOption ( task , 'collapseSkips' ) === false && ( this . getSelfOrParentOption ( task , 'showSkipMessage' ) || ! this . getSelfOrParentOption ( task , 'showSubtasks' ) ) ) { output = [ ... output , this . dumpData ( task , level , 'skip' ) ] ; } } if ( task ?. output ) { if ( ( task . isPending ( ) || task . isRetrying ( ) || task . isRollingBack ( ) ) && task . isPrompt ( ) ) { this . promptBar = task . output ; } else if ( this . isBottomBar ( task ) || ! task . hasTitle ( ) ) { const data = [ this . dumpData ( task , - 1 ) ] ; if ( ! this . bottomBar [ task . id ] ) { this . bottomBar [ task . id ] = { } ; this . bottomBar [ task . id ] . data = [ ] ; const bottomBar = this . getTaskOptions ( task ) . bottomBar ; if ( typeof bottomBar === 'boolean' ) { this . bottomBar [ task . id ] . items = 1 ; } else { this . bottomBar [ task . id ] . items = bottomBar ; } } if ( ! this . bottomBar [ task . id ] ?. data ?. some ( ( element ) => data . includes ( element ) ) && ! task . isSkipped ( ) ) { this . bottomBar [ task . id ] . data = [ ... this . bottomBar [ task . id ] . data , ... data ] ; } } else if ( task . isPending ( ) || task . isRetrying ( ) || task . isRollingBack ( ) || this . hasPersistentOutput ( task ) ) { output = [ ... output , this . dumpData ( task , level ) ] ; } } if ( this . getSelfOrParentOption ( task , 'showSubtasks' ) !== false && task . hasSubtasks ( ) && ( task . isPending ( ) || task . hasFailed ( ) || task . isCompleted ( ) && ! task . hasTitle ( ) || task . isCompleted ( ) && this . getSelfOrParentOption ( task , 'collapse' ) === false && ! task . subtasks . some ( ( subtask ) => subtask . rendererOptions . collapse === true ) || task . subtasks . some ( ( subtask ) => subtask . rendererOptions . collapse === false ) || task . subtasks . some ( ( subtask ) => subtask . hasFailed ( ) ) || task . subtasks . some ( ( subtask ) => subtask . hasRolledBack ( ) ) ) ) { const subtaskLevel = ! task . hasTitle ( ) ? level : level + 1 ; const subtaskRender = this . multiLineRenderer ( task . subtasks , subtaskLevel ) ; if ( subtaskRender ?. trim ( ) !== '' && ! task . subtasks . every ( ( subtask ) => ! subtask . hasTitle ( ) ) ) { output = [ ... output , subtaskRender ] ; } } if ( task . isCompleted ( ) || task . hasFailed ( ) || task . isSkipped ( ) || task . hasRolledBack ( ) ) { this . promptBar = null ; if ( ! this . hasPersistentOutput ( task ) ) { delete this . bottomBar [ task . id ] ; } } } } output = output . filter ( Boolean ) ; if ( output . length > 0 ) { return output . join ( EOL ) ; } else { return ; } } private renderBottomBar ( ) { if ( Object . keys ( this . bottomBar ) . length > 0 ) { this . bottomBar = Object . keys ( this . bottomBar ) . reduce ( ( o , key ) => { if ( ! o ?. [ key ] ) { o [ key ] = { } ; } o [ key ] = this . bottomBar [ key ] ; this . bottomBar [ key ] . data = this . bottomBar [ key ] . data . slice ( - this . bottomBar [ key ] . items ) ; o [ key ] . data = this . bottomBar [ key ] . data ; return o ; } , { } as Record < PropertyKey , { data ? : string [ ] ; items ? : number ; } | undefined > ) ; return Object . values ( this . bottomBar ) . reduce ( ( o , value ) => o = [ ... o , ... value . data ] , [ ] ) . filter ( Boolean ) . join ( EOL ) ; } } private renderPrompt ( ) { if ( this . promptBar ) { return this . promptBar ; } } private dumpData ( task , level , source = 'output' ) { let data ; switch ( source ) { case 'output' : data = task . output ; break ; case 'skip' : data = task . message . skip ; break ; case 'error' : data = task . message . error ; break ; } if ( task . hasTitle ( ) && source === 'error' && data === task . title ) { return ; } if ( typeof data === 'string' ) { return this . formatString ( data , this . getSymbol ( task , true ) , level + 1 ) ; } } private formatString ( str , icon , level ) { if ( str . trim ( ) === '' ) { return ; } str = `${ icon } ${ str }` ; let parsedStr ; let columns = process . stdout . columns ? process . stdout . columns : 80 ; columns = columns - level * this . options . indentation - 2 ; switch ( this . options . formatOutput ) { case 'truncate' : parsedStr = str . split ( EOL ) . map ( ( s , i ) => { return cliTruncate ( this . indentMultilineOutput ( s , i ) , columns ) ; } ) ; break ; case 'wrap' : parsedStr = cliWrap ( str , columns , { hard : true } ) . split ( EOL ) . map ( ( s , i ) => this . indentMultilineOutput ( s , i ) ) ; break ; default : throw new Error ( 'Format option for the renderer is wrong.' ) ; } if ( this . options . removeEmptyLines ) { parsedStr = parsedStr . filter ( Boolean ) ; } return indentString ( parsedStr . join ( EOL ) , level * this . options . indentation ) ; } private indentMultilineOutput ( str , i ) { return i > 0 ? indentString ( str . trim ( ) , 2 ) : str . trim ( ) ; } private getSymbol ( task , data = false ) { if ( task . isPending ( ) && ! data ) { return this . options ?. lazy || this . getSelfOrParentOption ( task , 'showSubtasks' ) !== false && task . hasSubtasks ( ) && ! task . subtasks . every ( ( subtask ) => ! subtask . hasTitle ( ) ) ? colorette . yellow ( figures . pointer ) : colorette . yellowBright ( this . spinner [ this . spinnerPosition ] ) ; } else if ( task . isCompleted ( ) && ! data ) { return task . hasSubtasks ( ) && task . subtasks . some ( ( subtask ) => subtask . hasFailed ( ) ) ? colorette . yellow ( figures . warning ) : colorette . green ( figures . tick ) ; } else if ( task . isRetrying ( ) && ! data ) { return this . options ?. lazy ? colorette . yellow ( figures . warning ) : colorette . yellow ( this . spinner [ this . spinnerPosition ] ) ; } else if ( task . isRollingBack ( ) && ! data ) { return this . options ?. lazy ? colorette . red ( figures . warning ) : colorette . red ( this . spinner [ this . spinnerPosition ] ) ; } else if ( task . hasRolledBack ( ) && ! data ) { return colorette . red ( figures . arrowLeft ) ; } else if ( task . hasFailed ( ) && ! data ) { return task . hasSubtasks ( ) ? colorette . red ( figures . pointer ) : colorette . red ( figures . cross ) ; } else if ( task . isSkipped ( ) && ! data && this . getSelfOrParentOption ( task , 'collapseSkips' ) === false ) { return colorette . yellow ( figures . warning ) ; } else if ( task . isSkipped ( ) && ( data || this . getSelfOrParentOption ( task , 'collapseSkips' ) ) ) { return colorette . yellow ( figures . arrowDown ) ; } return ! data ? colorette . dim ( figures . squareSmallFilled ) : figures . pointerSmall ; } private addSuffixToMessage ( message , suffix , condition ? ) { return condition ?? true ? message + colorette . dim ( ` [${ suffix }]` ) : message ; } }