license import { Terminal , ITerminalAddon , IBuffer , IBufferCell } from 'xterm' ; function constrain ( value , low , high ) { return Math . max ( low , Math . min ( value , high ) ) ; } abstract class BaseSerializeHandler { constructor ( protected readonly _buffer ) { } public serialize ( startRow , endRow ) { const cell1 = this . _buffer . getNullCell ( ) ; const cell2 = this . _buffer . getNullCell ( ) ; let oldCell = cell1 ; this . _beforeSerialize ( endRow - startRow , startRow , endRow ) ; for ( let row = startRow ; row < endRow ; row ++ ) { const line = this . _buffer . getLine ( row ) ; if ( line ) { for ( let col = 0 ; col < line . length ; col ++ ) { const c = line . getCell ( col , oldCell === cell1 ? cell2 : cell1 ) ; if ( ! c ) { console . warn ( `Can't get cell at row=${ row }, col=${ col }` ) ; continue ; } this . _nextCell ( c , oldCell , row , col ) ; oldCell = c ; } } this . _rowEnd ( row , row === endRow - 1 ) ; } this . _afterSerialize ( ) ; return this . _serializeString ( ) ; } protected _nextCell ( cell , oldCell , row , col ) { } protected _rowEnd ( row , isLastRow ) { } protected _beforeSerialize ( rows , startRow , endRow ) { } protected _afterSerialize ( ) { } protected _serializeString ( ) { return '' ; } } function equalFg ( cell1 , cell2 ) { return cell1 . getFgColorMode ( ) === cell2 . getFgColorMode ( ) && cell1 . getFgColor ( ) === cell2 . getFgColor ( ) ; } function equalBg ( cell1 , cell2 ) { return cell1 . getBgColorMode ( ) === cell2 . getBgColorMode ( ) && cell1 . getBgColor ( ) === cell2 . getBgColor ( ) ; } function equalFlags ( cell1 , cell2 ) { return cell1 . isInverse ( ) === cell2 . isInverse ( ) && cell1 . isBold ( ) === cell2 . isBold ( ) && cell1 . isUnderline ( ) === cell2 . isUnderline ( ) && cell1 . isBlink ( ) === cell2 . isBlink ( ) && cell1 . isInvisible ( ) === cell2 . isInvisible ( ) && cell1 . isItalic ( ) === cell2 . isItalic ( ) && cell1 . isDim ( ) === cell2 . isDim ( ) && cell1 . isStrikethrough ( ) === cell2 . isStrikethrough ( ) ; } class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex : number = 0 ; private _allRows : string [ ] = new Array < string > ( ) ; private _allRowSeparators : string [ ] = new Array < string > ( ) ; private _currentRow : string = '' ; private _nullCellCount : number = 0 ; private _cursorStyle : IBufferCell = this . _buffer . getNullCell ( ) ; private _cursorStyleRow : number = 0 ; private _cursorStyleCol : number = 0 ; private _backgroundCell : IBufferCell = this . _buffer . getNullCell ( ) ; private _firstRow : number = 0 ; private _lastCursorRow : number = 0 ; private _lastCursorCol : number = 0 ; private _lastContentCursorRow : number = 0 ; private _lastContentCursorCol : number = 0 ; constructor ( buffer , private readonly _terminal ) { super ( buffer ) ; } protected _beforeSerialize ( rows , start , end ) { this . _allRows = new Array < string > ( rows ) ; this . _lastContentCursorRow = start ; this . _lastCursorRow = start ; this . _firstRow = start ; } private _thisRowLastChar : IBufferCell = this . _buffer . getNullCell ( ) ; private _thisRowLastSecondChar : IBufferCell = this . _buffer . getNullCell ( ) ; private _nextRowFirstChar : IBufferCell = this . _buffer . getNullCell ( ) ; protected _rowEnd ( row , isLastRow ) { if ( this . _nullCellCount > 0 && ! equalBg ( this . _cursorStyle , this . _backgroundCell ) ) { this . _currentRow += `\u001b[${ this . _nullCellCount }X` ; } let rowSeparator = '' ; if ( ! isLastRow ) { if ( row - this . _firstRow >= this . _terminal . rows ) { this . _buffer . getLine ( this . _cursorStyleRow ) ?. getCell ( this . _cursorStyleCol , this . _backgroundCell ) ; } const currentLine = this . _buffer . getLine ( row ) ! ; const nextLine = this . _buffer . getLine ( row + 1 ) ! ; if ( ! nextLine . isWrapped ) { rowSeparator = '\r\n' ; this . _lastCursorRow = row + 1 ; this . _lastCursorCol = 0 ; } else { rowSeparator = '' ; const thisRowLastChar = currentLine . getCell ( currentLine . length - 1 , this . _thisRowLastChar ) ! ; const thisRowLastSecondChar = currentLine . getCell ( currentLine . length - 2 , this . _thisRowLastSecondChar ) ! ; const nextRowFirstChar = nextLine . getCell ( 0 , this . _nextRowFirstChar ) ! ; const isNextRowFirstCharDoubleWidth = nextRowFirstChar . getWidth ( ) > 1 ; let isValid = false ; if ( nextRowFirstChar . getChars ( ) && isNextRowFirstCharDoubleWidth ? this . _nullCellCount <= 1 : this . _nullCellCount <= 0 ) { if ( ( thisRowLastChar . getChars ( ) || thisRowLastChar . getWidth ( ) === 0 ) && equalBg ( thisRowLastChar , nextRowFirstChar ) ) { isValid = true ; } if ( isNextRowFirstCharDoubleWidth && ( thisRowLastSecondChar . getChars ( ) || thisRowLastSecondChar . getWidth ( ) === 0 ) && equalBg ( thisRowLastChar , nextRowFirstChar ) && equalBg ( thisRowLastSecondChar , nextRowFirstChar ) ) { isValid = true ; } } if ( ! isValid ) { rowSeparator = '-' . repeat ( this . _nullCellCount + 1 ) ; rowSeparator += '\u001b[1D\u001b[1X' ; if ( this . _nullCellCount > 0 ) { rowSeparator += '\u001b[A' ; rowSeparator += `\u001b[${ currentLine . length - this . _nullCellCount }C` ; rowSeparator += `\u001b[${ this . _nullCellCount }X` ; rowSeparator += `\u001b[${ currentLine . length - this . _nullCellCount }D` ; rowSeparator += '\u001b[B' ; } this . _lastContentCursorRow = row + 1 ; this . _lastContentCursorCol = 0 ; this . _lastCursorRow = row + 1 ; this . _lastCursorCol = 0 ; } } } this . _allRows [ this . _rowIndex ] = this . _currentRow ; this . _allRowSeparators [ this . _rowIndex ++ ] = rowSeparator ; this . _currentRow = '' ; this . _nullCellCount = 0 ; } private _diffStyle ( cell , oldCell ) { const sgrSeq = [ ] ; const fgChanged = ! equalFg ( cell , oldCell ) ; const bgChanged = ! equalBg ( cell , oldCell ) ; const flagsChanged = ! equalFlags ( cell , oldCell ) ; if ( fgChanged || bgChanged || flagsChanged ) { if ( cell . isAttributeDefault ( ) ) { if ( ! oldCell . isAttributeDefault ( ) ) { sgrSeq . push ( 0 ) ; } } else { if ( fgChanged ) { const color = cell . getFgColor ( ) ; if ( cell . isFgRGB ( ) ) { sgrSeq . push ( 38 , 2 , ( color >>> 16 ) & 0xFF , ( color >>> 8 ) & 0xFF , color & 0xFF ) ; } else if ( cell . isFgPalette ( ) ) { if ( color >= 16 ) { sgrSeq . push ( 38 , 5 , color ) ; } else { sgrSeq . push ( color & 8 ? 90 + ( color & 7 ) : 30 + ( color & 7 ) ) ; } } else { sgrSeq . push ( 39 ) ; } } if ( bgChanged ) { const color = cell . getBgColor ( ) ; if ( cell . isBgRGB ( ) ) { sgrSeq . push ( 48 , 2 , ( color >>> 16 ) & 0xFF , ( color >>> 8 ) & 0xFF , color & 0xFF ) ; } else if ( cell . isBgPalette ( ) ) { if ( color >= 16 ) { sgrSeq . push ( 48 , 5 , color ) ; } else { sgrSeq . push ( color & 8 ? 100 + ( color & 7 ) : 40 + ( color & 7 ) ) ; } } else { sgrSeq . push ( 49 ) ; } } if ( flagsChanged ) { if ( cell . isInverse ( ) !== oldCell . isInverse ( ) ) { sgrSeq . push ( cell . isInverse ( ) ? 7 : 27 ) ; } if ( cell . isBold ( ) !== oldCell . isBold ( ) ) { sgrSeq . push ( cell . isBold ( ) ? 1 : 22 ) ; } if ( cell . isUnderline ( ) !== oldCell . isUnderline ( ) ) { sgrSeq . push ( cell . isUnderline ( ) ? 4 : 24 ) ; } if ( cell . isBlink ( ) !== oldCell . isBlink ( ) ) { sgrSeq . push ( cell . isBlink ( ) ? 5 : 25 ) ; } if ( cell . isInvisible ( ) !== oldCell . isInvisible ( ) ) { sgrSeq . push ( cell . isInvisible ( ) ? 8 : 28 ) ; } if ( cell . isItalic ( ) !== oldCell . isItalic ( ) ) { sgrSeq . push ( cell . isItalic ( ) ? 3 : 23 ) ; } if ( cell . isDim ( ) !== oldCell . isDim ( ) ) { sgrSeq . push ( cell . isDim ( ) ? 2 : 22 ) ; } if ( cell . isStrikethrough ( ) !== oldCell . isStrikethrough ( ) ) { sgrSeq . push ( cell . isStrikethrough ( ) ? 9 : 29 ) ; } } } } return sgrSeq ; } protected _nextCell ( cell , oldCell , row , col ) { const isPlaceHolderCell = cell . getWidth ( ) === 0 ; if ( isPlaceHolderCell ) { return ; } const isEmptyCell = cell . getChars ( ) === '' ; const sgrSeq = this . _diffStyle ( cell , this . _cursorStyle ) ; const styleChanged = isEmptyCell ? ! equalBg ( this . _cursorStyle , cell ) : sgrSeq . length > 0 ; /**
         *  handles style change
         */ if ( styleChanged ) { if ( this . _nullCellCount > 0 ) { if ( ! equalBg ( this . _cursorStyle , this . _backgroundCell ) ) { this . _currentRow += `\u001b[${ this . _nullCellCount }X` ; } this . _currentRow += `\u001b[${ this . _nullCellCount }C` ; this . _nullCellCount = 0 ; } this . _lastContentCursorRow = this . _lastCursorRow = row ; this . _lastContentCursorCol = this . _lastCursorCol = col ; this . _currentRow += `\u001b[${ sgrSeq . join ( ';' ) }m` ; const line = this . _buffer . getLine ( row ) ; if ( line !== undefined ) { line . getCell ( col , this . _cursorStyle ) ; this . _cursorStyleRow = row ; this . _cursorStyleCol = col ; } } /**
         *  handles actual content
         */ if ( isEmptyCell ) { this . _nullCellCount += cell . getWidth ( ) ; } else { if ( this . _nullCellCount > 0 ) { if ( equalBg ( this . _cursorStyle , this . _backgroundCell ) ) { this . _currentRow += `\u001b[${ this . _nullCellCount }C` ; } else { this . _currentRow += `\u001b[${ this . _nullCellCount }X` ; this . _currentRow += `\u001b[${ this . _nullCellCount }C` ; } this . _nullCellCount = 0 ; } this . _currentRow += cell . getChars ( ) ; this . _lastContentCursorRow = this . _lastCursorRow = row ; this . _lastContentCursorCol = this . _lastCursorCol = col + cell . getWidth ( ) ; } } protected _serializeString ( ) { let rowEnd = this . _allRows . length ; if ( this . _buffer . length - this . _firstRow <= this . _terminal . rows ) { rowEnd = this . _lastContentCursorRow + 1 - this . _firstRow ; this . _lastCursorCol = this . _lastContentCursorCol ; this . _lastCursorRow = this . _lastContentCursorRow ; } let content = '' ; for ( let i = 0 ; i < rowEnd ; i ++ ) { content += this . _allRows [ i ] ; if ( i + 1 < rowEnd ) { content += this . _allRowSeparators [ i ] ; } } const realCursorRow = this . _buffer . baseY + this . _buffer . cursorY ; const realCursorCol = this . _buffer . cursorX ; const cursorMoved = ( realCursorRow !== this . _lastCursorRow || realCursorCol !== this . _lastCursorCol ) ; const moveRight = ( offset ) : void => { if ( offset > 0 ) { content += `\u001b[${ offset }C` ; } else if ( offset < 0 ) { content += `\u001b[${ - offset }D` ; } } ; const moveDown = ( offset ) : void => { if ( offset > 0 ) { content += `\u001b[${ offset }B` ; } else if ( offset < 0 ) { content += `\u001b[${ - offset }A` ; } } ; if ( cursorMoved ) { moveDown ( realCursorRow - this . _lastCursorRow ) ; moveRight ( realCursorCol - this . _lastCursorCol ) ; } return content ; } } export class SerializeAddon implements ITerminalAddon { private _terminal : Terminal | undefined ; constructor ( ) { } public activate ( terminal ) { this . _terminal = terminal ; } private _serializeBuffer ( terminal , buffer , scrollback ? ) { const maxRows = buffer . length ; const handler = new StringSerializeHandler ( buffer , terminal ) ; const correctRows = ( scrollback === undefined ) ? maxRows : constrain ( scrollback + terminal . rows , 0 , maxRows ) ; return handler . serialize ( maxRows - correctRows , maxRows ) ; } private _serializeModes ( terminal ) { let content = '' ; const modes = terminal . modes ; if ( modes . applicationCursorKeysMode ) content += '\x1b[?1h' ; if ( modes . applicationKeypadMode ) content += '\x1b[?66h' ; if ( modes . bracketedPasteMode ) content += '\x1b[?2004h' ; if ( modes . insertMode ) content += '\x1b[4h' ; if ( modes . originMode ) content += '\x1b[?6h' ; if ( modes . reverseWraparoundMode ) content += '\x1b[?45h' ; if ( modes . sendFocusMode ) content += '\x1b[?1004h' ; if ( modes . wraparoundMode === false ) content += '\x1b[?7l' ; if ( modes . mouseTrackingMode !== 'none' ) { switch ( modes . mouseTrackingMode ) { case 'x10' : content += '\x1b[?9h' ; break ; case 'vt200' : content += '\x1b[?1000h' ; break ; case 'drag' : content += '\x1b[?1002h' ; break ; case 'any' : content += '\x1b[?1003h' ; break ; } } return content ; } public serialize ( options ? ) { if ( ! this . _terminal ) { throw new Error ( 'Cannot use addon until it has been loaded' ) ; } let content = this . _serializeBuffer ( this . _terminal , this . _terminal . buffer . normal , options ?. scrollback ) ; if ( ! options ?. excludeAltBuffer ) { if ( this . _terminal . buffer . active . type === 'alternate' ) { const alternativeScreenContent = this . _serializeBuffer ( this . _terminal , this . _terminal . buffer . alternate , undefined ) ; content += `\u001b[?1049h\u001b[H${ alternativeScreenContent }` ; } } if ( ! options ?. excludeModes ) { content += this . _serializeModes ( this . _terminal ) ; } return content ; } public dispose ( ) { } } interface ISerializeOptions { scrollback ? : number ; excludeModes ? : boolean ; excludeAltBuffer ? : boolean ; }