import Table from 'cli-table2' ; import EasyTable from 'easy-table' ; import { EventEmitter } from 'events' ; import * as minimist from 'minimist' ; import { Terminal } from 'xterm' ; import { fit } from 'xterm/lib/addons/fit/fit' ; import { webLinksInit } from 'xterm/lib/addons/webLinks/webLinks' ; import { AsyncCommand , TCommand , TCommandOutput } from '../command/types' ; import { HelpTopic } from '../helpTopic' ; const EOL = '\r\n' ; export interface CliOptions { prompt ? : string ; welcomeMessage ? : string ; } export class Cli extends EventEmitter { private commands : { [ key ] : TCommand ; } = { } ; private helpTopics : { [ key ] : HelpTopic ; } = { } ; private buffer : string = '' ; private commandRunning : boolean = false ; private cursorOffset : number = 0 ; private options : CliOptions = { prompt : '> ' } ; private terminal : Terminal ; private terminalHistory : Array < string > = [ ] ; private terminalHistoryIndex = 0 ; constructor ( options ? ) { super ( ) ; if ( options ) { Object . assign ( this . options , options ) ; } this . terminal = new Terminal ( { convertEol : true , cursorBlink : true , cursorStyle : 'underline' , fontFamily : 'Menlo, "DejaVu Sans Mono", Consolas, "Lucida Console", monospace' , fontSize : 12 } ) ; this . terminal . on ( 'key' , ( d ) => this . input ( d ! ) ) ; this . terminal . on ( 'paste' , ( d ) => this . input ( d ! ) ) ; } private input ( str ) { switch ( str . charCodeAt ( 0 ) ) { case 8 : case 127 : if ( this . cursorOffset === 0 ) break ; this . cursorOffset -- ; this . buffer = this . buffer . substr ( 0 , this . cursorOffset ) + this . buffer . substr ( this . cursorOffset + 1 ) ; var textToRewrite = this . buffer . substr ( this . cursorOffset ) ; this . terminal . write ( '\b' + textToRewrite + ' ' + Array ( textToRewrite . length + 2 ) . join ( '\b' ) ) ; break ; case 13 : this . terminal . write ( EOL ) ; this . processInput ( ) ; break ; case 27 : if ( str . charCodeAt ( 1 ) === 91 ) { if ( str . charCodeAt ( 2 ) === 65 ) { var cmd = this . terminalHistory [ this . terminalHistoryIndex - 1 ] ; if ( cmd ) { this . terminalHistoryIndex -- ; this . terminal . write ( Array ( this . buffer . length + 1 ) . join ( '\b \b' ) ) ; this . terminal . write ( cmd ) ; this . cursorOffset = cmd . length ; this . buffer = cmd ; } } else if ( str . charCodeAt ( 2 ) === 66 ) { var cmd = this . terminalHistory [ this . terminalHistoryIndex + 1 ] ; if ( cmd ) { this . terminalHistoryIndex ++ ; this . terminal . write ( Array ( this . buffer . length + 1 ) . join ( '\b \b' ) ) ; this . terminal . write ( cmd ) ; this . cursorOffset = cmd . length ; this . buffer = cmd ; } } else if ( str . charCodeAt ( 2 ) === 67 ) { if ( this . cursorOffset === this . buffer . length ) break ; this . cursorOffset ++ ; this . terminal . write ( str ) ; } else if ( str . charCodeAt ( 2 ) === 68 ) { if ( this . cursorOffset === 0 ) break ; this . cursorOffset -- ; this . terminal . write ( str ) ; } } break ; default : var rest = this . buffer . substr ( this . cursorOffset ) ; this . buffer = this . buffer . substr ( 0 , this . cursorOffset ) + str + rest ; this . cursorOffset ++ ; this . terminal . write ( str + rest + Array ( rest . length + 1 ) . join ( '\b' ) ) ; break ; } } private processInput ( ) { if ( this . commandRunning ) return ; let buffer = this . buffer . trim ( ) ; if ( buffer ) { this . emit ( 'input' , buffer ) ; let args = minimist . default ( buffer . split ( ' ' ) ) ; let argsQueue = args . _ . slice ( 0 ) ; let command = undefined ; let commands = this . commands ; while ( argsQueue . length ) { let arg = argsQueue . shift ( ) ; if ( ! arg || ! commands [ arg ] ) { command = undefined ; break ; } command = commands [ arg ] ; commands = command . subCommands ; if ( ! commands ) break ; } if ( command !== undefined ) { this . commandRunning = true ; this . processCommand ( command , { ... args , _ : argsQueue } ) . catch ( ( err ) => { console . log ( err ) ; this . commandRunning = false ; this . write ( `ERR! ${ this . processCommandOutput ( err ) }${ EOL }` ) ; this . prompt ( ) ; } ) . then ( output => { this . commandRunning = false ; this . write ( `${ this . processCommandOutput ( output ) }` ) ; if ( buffer !== 'clear' ) this . write ( EOL ) ; this . prompt ( ) ; } ) ; } else { this . write ( `${ args . _ . join ( ' ' ) }: command not found${ EOL }` ) ; } this . terminalHistory . push ( buffer ) ; this . terminalHistoryIndex = this . terminalHistory . length ; } this . cursorOffset = 0 ; this . buffer = '' ; if ( ! this . commandRunning ) { this . prompt ( ) ; } } private processCommand ( command , args ) { if ( command instanceof AsyncCommand ) { return ( < AsyncCommand > command ) . run ( args ) ; } else { return Promise . resolve ( command . run ( args ) ) ; } } private processCommandOutput ( output ) { if ( typeof output === 'object' ) { if ( Array . isArray ( output ) ) { return this . processCommandOutputAsTable ( output ) ; } else { return JSON . stringify ( output , null , 2 ) . slice ( 1 , - 1 ) ; } } else { let o = < any > output ; return o || '' ; } } private processCommandOutputAsTable ( output ) { if ( ! output . length ) return '' ; const makeTable = ( colWidths ? ) => { const table = new Table ( { chars : { 'top' : ' ' , 'top-mid' : ' ' , 'top-left' : ' ' , 'top-right' : ' ' , 'bottom' : ' ' , 'bottom-mid' : ' ' , 'bottom-left' : ' ' , 'bottom-right' : ' ' , 'mid' : '-' , 'mid-mid' : '|' , 'middle' : '|' , 'left' : ' ' , 'left-mid' : ' ' , 'right' : ' ' , 'right-mid' : ' ' } , colWidths : colWidths , head : Object . keys ( output [ 0 ] ) , wordWrap : true , } ) ; output . forEach ( item => { table . push ( Object . values ( item ) ) ; } ) ; return table ; } ; let table = makeTable ( [ ] ) ; if ( table . width > this . terminal . cols ) { let accumulatedWidth = 0 ; let colsToComputeQueue = [ ] ; table . options . colWidths . forEach ( ( colWidth , index ) => { let n = accumulatedWidth + ( colWidth || 0 ) ; if ( n <= this . terminal . cols ) { accumulatedWidth = n ; } else { colsToComputeQueue . push ( index ) ; } } ) ; const remainingWidth = this . terminal . cols - table . options . colWidths . length - accumulatedWidth - 1 ; const colWidths = table . options . colWidths . map ( ( colWidth , index ) => { if ( colsToComputeQueue . indexOf ( index ) === - 1 ) { return colWidth as number ; } else { return Math . floor ( remainingWidth / colsToComputeQueue . length ) ; } } ) ; table = makeTable ( colWidths ) ; } return table . toString ( ) ; } private prompt ( ) { this . terminal . write ( `${ this . options . prompt }${ this . buffer }` ) ; } clear ( ) { this . terminal . reset ( ) ; } help ( ) { let table = new EasyTable ( ) ; table . separator = '\t\t' ; for ( let i in this . commands ) { table . cell ( 'name' , this . commands [ i ] . name ) ; table . cell ( 'description' , this . commands [ i ] . description ) ; table . newRow ( ) ; } this . terminal . write ( `${ EOL }These commands are defined.${ EOL }Type \`help name\` to find out more about the function \`name\`.${ EOL }${ EOL }\t${ table . print ( ) . replace ( /\r?\n/g , `${ EOL }\t` ) }` ) ; } helpTopic ( cmdName ) { if ( this . helpTopics [ cmdName ] ) { this . write ( `${ EOL }${ this . helpTopics [ cmdName ] }${ EOL }` ) ; } else { this . write ( `no help topics match \`${ cmdName }\`` ) ; } } register ( command ) { this . commands [ command . name ] = command ; if ( command . helpTopic ) { this . helpTopics [ command . name ] = command . helpTopic ; } } show ( domElement ) { this . terminal . open ( domElement ) ; this . terminal . focus ( ) ; if ( this . options . welcomeMessage && this . options . welcomeMessage . length ) { this . write ( `${ this . options . welcomeMessage }${ EOL }${ EOL }` ) ; } this . prompt ( ) ; fit ( this . terminal ) ; webLinksInit ( this . terminal ) ; window . onresize = ( ) => fit ( this . terminal ) ; } write ( str ) { this . terminal . write ( str ) ; } }