UNPKG

terminal-kit

Version:

256 colors, keys and mouse, input field, progress bars, screen buffer (including 32-bit composition and image loading), text buffer, and many more... Whether you just need colors and styles, build a simple interactive command line tool or a complexe termi

1,162 lines (888 loc) 35.3 kB
/* Terminal Kit Copyright (c) 2009 - 2022 Cédric Ronvel The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ "use strict" ; const ScreenBuffer = require( './ScreenBuffer.js' ) ; const misc = require( './misc.js' ) ; const fs = require( 'fs' ) ; const string = require( 'string-kit' ) ; /* options: * width: buffer width (default to dst.width) * height: buffer height (default to dst.height) * dst: writting destination * inline: for terminal dst only, draw inline instead of at some position (do not moveTo) * x: default position in the dst * y: default position in the dst * wrap: default wrapping behavior of .put() * noFill: do not call .fill() with default values at ScreenBuffer creation * blending: false/null or true or object (blending options): default blending params (can be overriden by .draw()) */ function ScreenBufferHD( options = {} ) { ScreenBuffer.call( this , options ) ; } module.exports = ScreenBufferHD ; const termkit = require( './termkit.js' ) ; ScreenBufferHD.prototype = Object.create( ScreenBuffer.prototype ) ; ScreenBufferHD.prototype.constructor = ScreenBufferHD ; ScreenBufferHD.prototype.bitsPerColor = 24 ; // Backward compatibility ScreenBufferHD.create = ( ... args ) => new ScreenBufferHD( ... args ) ; /* options: * attr: attributes passed to .put() * transparencyChar: a char that is transparent * transparencyType: bit flags for the transparency char */ ScreenBufferHD.createFromString = function( options , data ) { var x , y , length , attr , attrTrans , width , height , lineWidth , screenBuffer ; // Manage options if ( ! options ) { options = {} ; } if ( typeof data !== 'string' ) { if ( ! data.toString ) { throw new Error( '[terminal] ScreenBufferHD.createFromDataString(): argument #1 should be a string or provide a .toString() method.' ) ; } data = data.toString() ; } // Transform the data into an array of lines data = termkit.stripControlChars( data , true ).split( '\n' ) ; // Compute the buffer size width = 0 ; height = data.length ; attr = options.attr !== undefined ? options.attr : ScreenBufferHD.prototype.DEFAULT_ATTR ; if ( attr && typeof attr === 'object' && ! attr.BYTES_PER_ELEMENT ) { attr = ScreenBufferHD.object2attr( attr ) ; } attrTrans = attr ; if ( options.transparencyChar ) { if ( ! options.transparencyType ) { attrTrans |= ScreenBufferHD.prototype.TRANSPARENCY ; } else { attrTrans |= options.transparencyType & ScreenBufferHD.prototype.TRANSPARENCY ; } } // Compute the width of the screenBuffer for ( y = 0 ; y < data.length ; y ++ ) { lineWidth = string.unicode.width( data[ y ] ) ; if ( lineWidth > width ) { width = lineWidth ; } } // Create the buffer with the right width & height screenBuffer = new ScreenBufferHD( { width: width , height: height } ) ; // Fill the buffer with data for ( y = 0 ; y < data.length ; y ++ ) { if ( ! options.transparencyChar ) { screenBuffer.put( { x: 0 , y: y , attr: attr } , data[ y ] ) ; } else { length = data[ y ].length ; for ( x = 0 ; x < length ; x ++ ) { if ( data[ y ][ x ] === options.transparencyChar ) { screenBuffer.put( { x: x , y: y , attr: attrTrans } , data[ y ][ x ] ) ; } else { screenBuffer.put( { x: x , y: y , attr: attr } , data[ y ][ x ] ) ; } } } } return screenBuffer ; } ; // Backward compatibility ScreenBufferHD.createFromChars = ScreenBufferHD.createFromString ; var colorScheme = require( './colorScheme/gnome.json' ).map( o => ( { color: { r: o.r , g: o.g , b: o.b } } ) ) ; var bgColorScheme = colorScheme.map( o => ( { bgColor: { r: o.r , g: o.g , b: o.b } } ) ) ; ScreenBufferHD.prototype.markupToAttrObject = { normal: { '-': { dim: true } , '+': { bold: true } , '_': { underline: true } , '/': { italic: true } , '!': { inverse: true } , 'k': colorScheme[ 0 ] , 'r': colorScheme[ 1 ] , 'g': colorScheme[ 2 ] , 'y': colorScheme[ 3 ] , 'b': colorScheme[ 4 ] , 'm': colorScheme[ 5 ] , 'c': colorScheme[ 6 ] , 'w': colorScheme[ 7 ] , 'K': colorScheme[ 8 ] , 'R': colorScheme[ 9 ] , 'G': colorScheme[ 10 ] , 'Y': colorScheme[ 11 ] , 'B': colorScheme[ 12 ] , 'M': colorScheme[ 13 ] , 'C': colorScheme[ 14 ] , 'W': colorScheme[ 15 ] } , background: { 'k': bgColorScheme[ 0 ] , 'r': bgColorScheme[ 1 ] , 'g': bgColorScheme[ 2 ] , 'y': bgColorScheme[ 3 ] , 'b': bgColorScheme[ 4 ] , 'm': bgColorScheme[ 5 ] , 'c': bgColorScheme[ 6 ] , 'w': bgColorScheme[ 7 ] , 'K': bgColorScheme[ 8 ] , 'R': bgColorScheme[ 9 ] , 'G': bgColorScheme[ 10 ] , 'Y': bgColorScheme[ 11 ] , 'B': bgColorScheme[ 12 ] , 'M': bgColorScheme[ 13 ] , 'C': bgColorScheme[ 14 ] , 'W': bgColorScheme[ 15 ] } } ; ScreenBufferHD.prototype.blitterCellBlendingIterator = function( p ) { var attr = this.readAttr( p.context.srcBuffer , p.srcStart ) ; var blendFn = ScreenBufferHD.blendFn.normal ; var opacity = 1 ; var blendSrcFgWithDstBg = false ; if ( typeof p.context.blending === 'object' ) { if ( p.context.blending.fn ) { blendFn = p.context.blending.fn ; } if ( p.context.blending.opacity !== undefined ) { opacity = p.context.blending.opacity ; } if ( p.context.blending.blendSrcFgWithDstBg ) { blendSrcFgWithDstBg = true ; } } if ( ( attr[ BPOS_MISC ] & STYLE_TRANSPARENCY ) && ( attr[ BPOS_MISC ] & CHAR_TRANSPARENCY ) && ( ! opacity || ( attr[ BPOS_A ] === 0 && attr[ BPOS_BG_A ] === 0 ) ) ) { // Fully transparent, do nothing return ; } // First, manage fullwidth chars if ( p.startOfBlitLine && p.dstStart >= this.ITEM_SIZE ) { // Remove overlapping dst fullwidth at the begining this.removeLeadingFullWidth( p.context.dstBuffer , p.dstStart - this.ITEM_SIZE ) ; } if ( p.endOfBlitLine && p.dstEnd < p.context.dstBuffer.length ) { // Remove overlapping dst fullwidth at the end this.removeTrailingFullWidth( p.context.dstBuffer , p.dstEnd ) ; } if ( ! ( attr[ BPOS_MISC ] & STYLE_TRANSPARENCY ) && ! ( attr[ BPOS_MISC ] & CHAR_TRANSPARENCY ) && attr[ BPOS_A ] === 255 && attr[ BPOS_BG_A ] === 255 && opacity === 1 && blendFn === ScreenBufferHD.blendFn.normal && ! ( p.endOfBlitLine || ! ( attr[ BPOS_MISC ] & LEADING_FULLWIDTH ) ) ) { // Fully opaque, copy it p.context.srcBuffer.copy( p.context.dstBuffer , p.dstStart , p.srcStart , p.srcEnd ) ; return ; } // Blending part... var alpha ; // Normalized alpha if ( attr[ BPOS_A ] ) { alpha = opacity * attr[ BPOS_A ] / 255 ; if ( blendSrcFgWithDstBg ) { p.context.dstBuffer[ p.dstStart + BPOS_R ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_R ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_R ] , alpha , blendFn ) ; p.context.dstBuffer[ p.dstStart + BPOS_G ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_G ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_G ] , alpha , blendFn ) ; p.context.dstBuffer[ p.dstStart + BPOS_B ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_B ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_B ] , alpha , blendFn ) ; // Blending alpha is special p.context.dstBuffer[ p.dstStart + BPOS_A ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_A ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_A ] , opacity , ScreenBufferHD.blendFn.screen ) ; } else { p.context.dstBuffer[ p.dstStart + BPOS_R ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_R ] , p.context.dstBuffer[ p.dstStart + BPOS_R ] , alpha , blendFn ) ; p.context.dstBuffer[ p.dstStart + BPOS_G ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_G ] , p.context.dstBuffer[ p.dstStart + BPOS_G ] , alpha , blendFn ) ; p.context.dstBuffer[ p.dstStart + BPOS_B ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_B ] , p.context.dstBuffer[ p.dstStart + BPOS_B ] , alpha , blendFn ) ; // Blending alpha is special p.context.dstBuffer[ p.dstStart + BPOS_A ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_A ] , p.context.dstBuffer[ p.dstStart + BPOS_A ] , opacity , ScreenBufferHD.blendFn.screen ) ; } } if ( attr[ BPOS_BG_A ] ) { alpha = opacity * attr[ BPOS_BG_A ] / 255 ; p.context.dstBuffer[ p.dstStart + BPOS_BG_R ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_BG_R ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_R ] , alpha , blendFn ) ; p.context.dstBuffer[ p.dstStart + BPOS_BG_G ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_BG_G ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_G ] , alpha , blendFn ) ; p.context.dstBuffer[ p.dstStart + BPOS_BG_B ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_BG_B ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_B ] , alpha , blendFn ) ; // Blending alpha is special p.context.dstBuffer[ p.dstStart + BPOS_BG_A ] = alphaBlend( p.context.srcBuffer[ p.srcStart + BPOS_BG_A ] , p.context.dstBuffer[ p.dstStart + BPOS_BG_A ] , opacity , ScreenBufferHD.blendFn.screen ) ; } if ( ! ( attr[ BPOS_MISC ] & STYLE_TRANSPARENCY ) ) { p.context.dstBuffer[ p.dstStart + BPOS_STYLE ] = p.context.srcBuffer[ p.srcStart + BPOS_STYLE ] ; } if ( ! ( attr[ BPOS_MISC ] & CHAR_TRANSPARENCY ) ) { if ( p.endOfBlitLine && ( attr[ BPOS_MISC ] & LEADING_FULLWIDTH ) ) { // Leading fullwidth at the end of the blit line, output a space instead this.writeChar( p.context.dstBuffer , ' ' , p.dstStart ) ; } else { // Copy source character p.context.srcBuffer.copy( p.context.dstBuffer , p.dstStart + this.ATTR_SIZE , p.srcStart + this.ATTR_SIZE , p.srcEnd ) ; } } } ; function alphaBlend( src , dst , alpha , fn ) { return Math.round( fn( src , dst ) * alpha + dst * ( 1 - alpha ) ) ; } // https://en.wikipedia.org/wiki/Blend_modes ScreenBufferHD.blendFn = { normal: src => src , multiply: ( src , dst ) => 255 * ( ( src / 255 ) * ( dst / 255 ) ) , screen: ( src , dst ) => 255 * ( 1 - ( 1 - src / 255 ) * ( 1 - dst / 255 ) ) , overlay: ( src , dst ) => dst <= 127 ? 255 * ( 2 * ( src / 255 ) * ( dst / 255 ) ) : 255 * ( 1 - 2 * ( 1 - src / 255 ) * ( 1 - dst / 255 ) ) , hardLight: ( src , dst ) => src <= 127 ? 255 * ( 2 * ( src / 255 ) * ( dst / 255 ) ) : 255 * ( 1 - 2 * ( 1 - src / 255 ) * ( 1 - dst / 255 ) ) , softLight: ( src , dst ) => { src /= 255 ; dst /= 255 ; return 255 * ( ( 1 - 2 * src ) * dst * dst + 2 * src * dst ) ; } } ; function attrEquals( attr1 , attr2 ) { if ( attr1.readUInt32BE( BPOS_FG ) !== attr2.readUInt32BE( BPOS_FG ) ) { return false ; } if ( attr1.readUInt32BE( BPOS_BG ) !== attr2.readUInt32BE( BPOS_BG ) ) { return false ; } if ( attr1[ BPOS_STYLE ] !== attr2[ BPOS_STYLE ] ) { return false ; } if ( ( attr1[ BPOS_MISC ] & MISC_ATTR_MASK ) !== ( attr2[ BPOS_MISC ] & MISC_ATTR_MASK ) ) { return false ; } return true ; } ScreenBufferHD.prototype.terminalBlitterLineIterator = function( p ) { var offset , attr ; if ( ! p.context.inline ) { p.context.sequence += p.context.term.optimized.moveTo( p.dstXmin , p.dstY ) ; p.context.moves ++ ; } for ( offset = p.srcStart ; offset < p.srcEnd ; offset += this.ITEM_SIZE ) { attr = this.readAttr( p.context.srcBuffer , offset ) ; if ( attr[ BPOS_MISC ] & TRAILING_FULLWIDTH ) { // Trailing fullwidth cell, the previous char already shifted the cursor to the right continue ; } if ( ! p.context.lastAttr || ! attrEquals( attr , p.context.lastAttr ) ) { p.context.sequence += ! p.context.lastAttr || ! p.context.deltaEscapeSequence ? this.generateEscapeSequence( p.context.term , attr ) : this.generateDeltaEscapeSequence( p.context.term , attr , p.context.lastAttr ) ; p.context.lastAttr = attr ; p.context.attrs ++ ; } p.context.sequence += this.readChar( p.context.srcBuffer , offset ) ; p.context.cells ++ ; } if ( p.context.inline ) { // When we are at the bottom of the screen, the terminal may create a new line // using the current attr for background color, just like .eraseLineAfter() would do... // So we have to reset *BEFORE* the new line. p.context.sequence += p.context.term.optimized.styleReset + '\n' ; p.context.attrs ++ ; p.context.lastAttr = null ; } // Output buffering saves a good amount of CPU usage both for the node's processus and the terminal processus if ( p.context.sequence.length > OUTPUT_THRESHOLD ) { p.context.rawTerm( p.context.sequence ) ; p.context.sequence = '' ; p.context.writes ++ ; } } ; ScreenBufferHD.prototype.terminalBlitterCellIterator = function( p ) { //var attr = p.context.srcBuffer.readUInt32BE( p.srcStart ) ; var attr = this.readAttr( p.context.srcBuffer , p.srcStart ) ; // If last buffer's cell === current buffer's cell, no need to refresh... skip that now if ( p.context.srcLastBuffer ) { if ( attrEquals( attr , this.readAttr( p.context.srcLastBuffer , p.srcStart ) ) && this.readChar( p.context.srcBuffer , p.srcStart ) === this.readChar( p.context.srcLastBuffer , p.srcStart ) ) { return ; } p.context.srcBuffer.copy( p.context.srcLastBuffer , p.srcStart , p.srcStart , p.srcEnd ) ; } if ( attr[ BPOS_MISC ] & TRAILING_FULLWIDTH ) { // Trailing fullwidth, do nothing // Check that after eventually updating lastBuffer return ; } p.context.cells ++ ; if ( p.dstX !== p.context.cx || p.dstY !== p.context.cy ) { p.context.sequence += p.context.term.optimized.moveTo( p.dstX , p.dstY ) ; p.context.moves ++ ; } if ( ! p.context.lastAttr || ! attrEquals( attr , p.context.lastAttr ) ) { p.context.sequence += ! p.context.lastAttr || ! p.context.deltaEscapeSequence ? this.generateEscapeSequence( p.context.term , attr ) : this.generateDeltaEscapeSequence( p.context.term , attr , p.context.lastAttr ) ; p.context.lastAttr = attr ; p.context.attrs ++ ; } p.context.sequence += this.readChar( p.context.srcBuffer , p.srcStart ) ; // Output buffering saves a good amount of CPU usage both for the node's processus and the terminal processus if ( p.context.sequence.length > OUTPUT_THRESHOLD ) { p.context.rawTerm( p.context.sequence ) ; p.context.sequence = '' ; p.context.writes ++ ; } // Next expected cursor position p.context.cy = p.dstY ; if ( attr & LEADING_FULLWIDTH ) { p.context.cx = p.dstX + 2 ; return true ; // i.e.: tell the master iterator that this is a full-width char } p.context.cx = p.dstX + 1 ; } ; ScreenBufferHD.fromNdarrayImage = function( pixels /*, options */ ) { var x , xMax = pixels.shape[ 0 ] , y , yMax = Math.ceil( pixels.shape[ 1 ] / 2 ) , hasAlpha = pixels.shape[ 2 ] === 4 ; var image = new ScreenBufferHD( { width: xMax , height: yMax , blending: true , noFill: true } ) ; for ( x = 0 ; x < xMax ; x ++ ) { for ( y = 0 ; y < yMax ; y ++ ) { if ( y * 2 + 1 < pixels.shape[ 1 ] ) { image.put( { x: x , y: y , attr: { color: { r: pixels.get( x , y * 2 , 0 ) , g: pixels.get( x , y * 2 , 1 ) , b: pixels.get( x , y * 2 , 2 ) , a: hasAlpha ? pixels.get( x , y * 2 , 3 ) : 255 } , bgColor: { r: pixels.get( x , y * 2 + 1 , 0 ) , g: pixels.get( x , y * 2 + 1 , 1 ) , b: pixels.get( x , y * 2 + 1 , 2 ) , a: hasAlpha ? pixels.get( x , y * 2 + 1 , 3 ) : 255 } } } , '▀' ) ; } else { image.put( { x: x , y: y , attr: { color: { r: pixels.get( x , y * 2 , 0 ) , g: pixels.get( x , y * 2 , 1 ) , b: pixels.get( x , y * 2 , 2 ) , a: hasAlpha ? pixels.get( x , y * 2 , 3 ) : 255 } , bgColor: { r: 0 , g: 0 , b: 0 , a: 0 } } } , '▀' ) ; } } } return image ; } ; ScreenBufferHD.loadImage = termkit.image.load.bind( ScreenBufferHD , ScreenBufferHD.fromNdarrayImage ) ; ScreenBufferHD.prototype.dump = function() { var y , x , offset , str = '' , char ; for ( y = 0 ; y < this.height ; y ++ ) { for ( x = 0 ; x < this.width ; x ++ ) { offset = ( y * this.width + x ) * this.ITEM_SIZE ; char = this.readChar( this.buffer , offset ) ; str += char + ( string.unicode.isFullWidth( char ) ? ' ' : ' ' ) ; str += string.format( '%x%x%x%x %x%x%x%x %x%x ' , this.buffer.readUInt8( offset ) , this.buffer.readUInt8( offset + 1 ) , this.buffer.readUInt8( offset + 2 ) , this.buffer.readUInt8( offset + 3 ) , this.buffer.readUInt8( offset + 4 ) , this.buffer.readUInt8( offset + 5 ) , this.buffer.readUInt8( offset + 6 ) , this.buffer.readUInt8( offset + 7 ) , this.buffer.readUInt8( offset + 8 ) , this.buffer.readUInt8( offset + 9 ) ) ; } str += '\n' ; } return str ; } ; ScreenBufferHD.prototype.readAttr = function( buffer , at ) { return buffer.slice( at , at + this.ATTR_SIZE ) ; } ; ScreenBufferHD.prototype.writeAttr = function( buffer , attr , at , fullWidth = 0 ) { if ( ! fullWidth ) { return attr.copy( buffer , at ) ; } attr.copy( buffer , at ) ; return buffer.writeUInt8( attr[ BPOS_MISC ] | fullWidth , at + BPOS_MISC ) ; } ; ScreenBufferHD.prototype.hasLeadingFullWidth = function( buffer , at ) { return !! ( buffer.readUInt8( at + BPOS_MISC ) & LEADING_FULLWIDTH ) ; } ; ScreenBufferHD.prototype.hasTrailingFullWidth = function( buffer , at ) { return !! ( buffer.readUInt8( at + BPOS_MISC ) & TRAILING_FULLWIDTH ) ; } ; ScreenBufferHD.prototype.removeLeadingFullWidth = function( buffer , at ) { var attr = buffer.readUInt8( at + BPOS_MISC ) ; if ( ! ( attr & LEADING_FULLWIDTH ) ) { return ; } attr ^= LEADING_FULLWIDTH ; buffer.writeUInt8( attr , at + BPOS_MISC ) ; buffer.write( ' ' , at + this.ATTR_SIZE , this.CHAR_SIZE ) ; } ; ScreenBufferHD.prototype.removeTrailingFullWidth = function( buffer , at ) { var attr = buffer.readUInt8( at + BPOS_MISC ) ; if ( ! ( attr & TRAILING_FULLWIDTH ) ) { return ; } attr ^= TRAILING_FULLWIDTH ; buffer.writeUInt8( attr , at + BPOS_MISC ) ; buffer.write( ' ' , at + this.ATTR_SIZE , this.CHAR_SIZE ) ; } ; ScreenBufferHD.prototype.removeFullWidth = function( buffer , at ) { var attr = buffer.readUInt8( at + BPOS_MISC ) ; if ( ! ( attr & FULLWIDTH ) ) { return ; } attr = attr & REMOVE_FULLWIDTH_FLAG ; buffer.writeUInt8( attr , at + BPOS_MISC ) ; buffer.write( ' ' , at + this.ATTR_SIZE , this.CHAR_SIZE ) ; } ; ScreenBufferHD.prototype.attrLeadingFullWidth = function( attr ) { attr.writeUInt8( attr[ BPOS_MISC ] | LEADING_FULLWIDTH , BPOS_MISC ) ; return attr ; } ; ScreenBufferHD.prototype.attrTrailingFullWidth = function( attr ) { attr.writeUInt8( attr[ BPOS_MISC ] | TRAILING_FULLWIDTH , BPOS_MISC ) ; return attr ; } ; ScreenBufferHD.prototype.readChar = function( buffer , at ) { var bytes ; at += this.ATTR_SIZE ; if ( buffer[ at ] < 0x80 ) { bytes = 1 ; } else if ( buffer[ at ] < 0xc0 ) { return '\x00' ; } // We are in a middle of an unicode multibyte sequence... something was wrong... else if ( buffer[ at ] < 0xe0 ) { bytes = 2 ; } else if ( buffer[ at ] < 0xf0 ) { bytes = 3 ; } else if ( buffer[ at ] < 0xf8 ) { bytes = 4 ; } else if ( buffer[ at ] < 0xfc ) { bytes = 5 ; } else { bytes = 6 ; } if ( bytes > this.CHAR_SIZE ) { return '\x00' ; } return buffer.toString( 'utf8' , at , at + bytes ) ; } ; ScreenBufferHD.prototype.writeChar = function( buffer , char , at ) { return buffer.write( char , at + this.ATTR_SIZE , this.CHAR_SIZE ) ; } ; ScreenBufferHD.prototype.generateEscapeSequence = function( term , attr ) { var esc = term.optimized.styleReset + term.optimized.color24bits( attr[ BPOS_R ] , attr[ BPOS_G ] , attr[ BPOS_B ] ) + term.optimized.bgColor24bits( attr[ BPOS_BG_R ] , attr[ BPOS_BG_G ] , attr[ BPOS_BG_B ] ) ; var style = attr[ BPOS_STYLE ] ; // Style part if ( style & BOLD ) { esc += term.optimized.bold ; } if ( style & DIM ) { esc += term.optimized.dim ; } if ( style & ITALIC ) { esc += term.optimized.italic ; } if ( style & UNDERLINE ) { esc += term.optimized.underline ; } if ( style & BLINK ) { esc += term.optimized.blink ; } if ( style & INVERSE ) { esc += term.optimized.inverse ; } if ( style & HIDDEN ) { esc += term.optimized.hidden ; } if ( style & STRIKE ) { esc += term.optimized.strike ; } return esc ; } ; // Generate only the delta between the last and new attributes, may speed up things for the terminal process // as well as consume less bandwidth, at the cost of small CPU increase in the application process ScreenBufferHD.prototype.generateDeltaEscapeSequence = function( term , attr , lastAttr ) { var esc = '' ; // Color if ( attr[ BPOS_R ] !== lastAttr[ BPOS_R ] || attr[ BPOS_G ] !== lastAttr[ BPOS_G ] || attr[ BPOS_B ] !== lastAttr[ BPOS_B ] ) { esc += term.optimized.color24bits( attr[ BPOS_R ] , attr[ BPOS_G ] , attr[ BPOS_B ] ) ; } // Bg color if ( attr[ BPOS_BG_R ] !== lastAttr[ BPOS_BG_R ] || attr[ BPOS_BG_G ] !== lastAttr[ BPOS_BG_G ] || attr[ BPOS_BG_B ] !== lastAttr[ BPOS_BG_B ] ) { esc += term.optimized.bgColor24bits( attr[ BPOS_BG_R ] , attr[ BPOS_BG_G ] , attr[ BPOS_BG_B ] ) ; } var style = attr[ BPOS_STYLE ] ; var lastStyle = lastAttr[ BPOS_STYLE ] ; if ( style !== lastStyle ) { // Bold and dim style are particular: all terminal has noBold = noDim if ( ( style & BOLD_DIM ) !== ( lastStyle & BOLD_DIM ) ) { if ( ( ( lastStyle & BOLD ) && ! ( style & BOLD ) ) || ( ( lastStyle & DIM ) && ! ( style & DIM ) ) ) { esc += term.optimized.noBold ; if ( style & BOLD ) { esc += term.optimized.bold ; } if ( style & DIM ) { esc += term.optimized.dim ; } } else { if ( ( style & BOLD ) && ! ( lastStyle & BOLD ) ) { esc += term.optimized.bold ; } if ( ( style & DIM ) && ! ( lastStyle & DIM ) ) { esc += term.optimized.dim ; } } } if ( ( style & ITALIC ) !== ( lastStyle & ITALIC ) ) { esc += style & ITALIC ? term.optimized.italic : term.optimized.noItalic ; } if ( ( style & UNDERLINE ) !== ( lastStyle & UNDERLINE ) ) { esc += style & UNDERLINE ? term.optimized.underline : term.optimized.noUnderline ; } if ( ( style & BLINK ) !== ( lastStyle & BLINK ) ) { esc += style & BLINK ? term.optimized.blink : term.optimized.noBlink ; } if ( ( style & INVERSE ) !== ( lastStyle & INVERSE ) ) { esc += style & INVERSE ? term.optimized.inverse : term.optimized.noInverse ; } if ( ( style & HIDDEN ) !== ( lastStyle & HIDDEN ) ) { esc += style & HIDDEN ? term.optimized.hidden : term.optimized.noHidden ; } if ( ( style & STRIKE ) !== ( lastStyle & STRIKE ) ) { esc += style & STRIKE ? term.optimized.strike : term.optimized.noStrike ; } } return esc ; } ; /* Methods that are both static and instance member. It must be possible to call them without any instance AND invoke instance specific method. */ ScreenBufferHD.attr2object = function( attr ) { var object = { color: {} , bgColor: {} } ; // Color part object.color.r = attr[ BPOS_R ] ; object.color.g = attr[ BPOS_G ] ; object.color.b = attr[ BPOS_B ] ; object.color.a = attr[ BPOS_A ] ; // Background color part object.bgColor.r = attr[ BPOS_BG_R ] ; object.bgColor.g = attr[ BPOS_BG_G ] ; object.bgColor.b = attr[ BPOS_BG_B ] ; object.bgColor.a = attr[ BPOS_BG_A ] ; // Style part object.bold = !! ( attr[ BPOS_STYLE ] & BOLD ) ; object.dim = !! ( attr[ BPOS_STYLE ] & DIM ) ; object.italic = !! ( attr[ BPOS_STYLE ] & ITALIC ) ; object.underline = !! ( attr[ BPOS_STYLE ] & UNDERLINE ) ; object.blink = !! ( attr[ BPOS_STYLE ] & BLINK ) ; object.inverse = !! ( attr[ BPOS_STYLE ] & INVERSE ) ; object.hidden = !! ( attr[ BPOS_STYLE ] & HIDDEN ) ; object.strike = !! ( attr[ BPOS_STYLE ] & STRIKE ) ; // Misc part object.styleTransparency = !! ( attr[ BPOS_MISC ] & STYLE_TRANSPARENCY ) ; object.charTransparency = !! ( attr[ BPOS_MISC ] & CHAR_TRANSPARENCY ) ; return object ; } ; ScreenBufferHD.prototype.attr2object = ScreenBufferHD.attr2object ; ScreenBufferHD.object2attr = function( object ) { var attr = Buffer.allocUnsafe( ScreenBufferHD.prototype.ATTR_SIZE ) ; if ( ! object || typeof object !== 'object' ) { object = {} ; } // Misc and color part attr[ BPOS_MISC ] = 0 ; // Color part if ( typeof object.color === 'string' ) { let color = misc.hexToRgba( object.color ) ; attr[ BPOS_R ] = color.r ; attr[ BPOS_G ] = color.g ; attr[ BPOS_B ] = color.b ; attr[ BPOS_A ] = color.a ; } else if ( object.color && typeof object.color === 'object' ) { attr[ BPOS_R ] = + object.color.r || 0 ; attr[ BPOS_G ] = + object.color.g || 0 ; attr[ BPOS_B ] = + object.color.b || 0 ; attr[ BPOS_A ] = object.color.a !== undefined ? + object.color.a || 0 : 255 ; } else { attr[ BPOS_R ] = 0 ; attr[ BPOS_G ] = 0 ; attr[ BPOS_B ] = 0 ; attr[ BPOS_A ] = 255 ; } // Background color part if ( typeof object.bgColor === 'string' ) { let color = misc.hexToRgba( object.bgColor ) ; attr[ BPOS_BG_R ] = color.r ; attr[ BPOS_BG_G ] = color.g ; attr[ BPOS_BG_B ] = color.b ; attr[ BPOS_BG_A ] = color.a ; } else if ( object.bgColor && typeof object.bgColor === 'object' ) { attr[ BPOS_BG_R ] = + object.bgColor.r || 0 ; attr[ BPOS_BG_G ] = + object.bgColor.g || 0 ; attr[ BPOS_BG_B ] = + object.bgColor.b || 0 ; attr[ BPOS_BG_A ] = object.bgColor.a !== undefined ? + object.bgColor.a || 0 : 255 ; } else { attr[ BPOS_BG_R ] = 0 ; attr[ BPOS_BG_G ] = 0 ; attr[ BPOS_BG_B ] = 0 ; attr[ BPOS_BG_A ] = 255 ; } if ( object.styleTransparency ) { attr[ BPOS_MISC ] |= STYLE_TRANSPARENCY ; } if ( object.charTransparency ) { attr[ BPOS_MISC ] |= CHAR_TRANSPARENCY ; } // Style part attr[ BPOS_STYLE ] = 0 ; if ( object.bold ) { attr[ BPOS_STYLE ] |= BOLD ; } if ( object.dim ) { attr[ BPOS_STYLE ] |= DIM ; } if ( object.italic ) { attr[ BPOS_STYLE ] |= ITALIC ; } if ( object.underline ) { attr[ BPOS_STYLE ] |= UNDERLINE ; } if ( object.blink ) { attr[ BPOS_STYLE ] |= BLINK ; } if ( object.inverse ) { attr[ BPOS_STYLE ] |= INVERSE ; } if ( object.hidden ) { attr[ BPOS_STYLE ] |= HIDDEN ; } if ( object.strike ) { attr[ BPOS_STYLE ] |= STRIKE ; } return attr ; } ; ScreenBufferHD.prototype.object2attr = ScreenBufferHD.object2attr ; ScreenBufferHD.attrAndObject = function( attr , object ) { if ( ! object || typeof object !== 'object' ) { return attr ; } // Misc and color part if ( object.color && typeof object.color === 'object' ) { if ( object.color.r !== undefined ) { attr[ BPOS_R ] = + object.color.r || 0 ; } if ( object.color.g !== undefined ) { attr[ BPOS_G ] = + object.color.g || 0 ; } if ( object.color.b !== undefined ) { attr[ BPOS_B ] = + object.color.b || 0 ; } if ( object.color.a !== undefined ) { attr[ BPOS_A ] = + object.color.a || 0 ; } } if ( object.bgColor && typeof object.bgColor === 'object' ) { if ( object.bgColor.r !== undefined ) { attr[ BPOS_BG_R ] = + object.bgColor.r || 0 ; } if ( object.bgColor.g !== undefined ) { attr[ BPOS_BG_G ] = + object.bgColor.g || 0 ; } if ( object.bgColor.b !== undefined ) { attr[ BPOS_BG_B ] = + object.bgColor.b || 0 ; } if ( object.bgColor.a !== undefined ) { attr[ BPOS_BG_A ] = + object.bgColor.a || 0 ; } } if ( object.styleTransparency === true ) { attr[ BPOS_MISC ] |= STYLE_TRANSPARENCY ; } else if ( object.styleTransparency === false ) { attr[ BPOS_MISC ] &= ~ STYLE_TRANSPARENCY ; } if ( object.charTransparency === true ) { attr[ BPOS_MISC ] |= CHAR_TRANSPARENCY ; } else if ( object.charTransparency === false ) { attr[ BPOS_MISC ] &= ~ CHAR_TRANSPARENCY ; } // Style part if ( object.bold === true ) { attr[ BPOS_STYLE ] |= BOLD ; } else if ( object.bold === false ) { attr[ BPOS_STYLE ] &= ~ BOLD ; } if ( object.dim === true ) { attr[ BPOS_STYLE ] |= DIM ; } else if ( object.dim === false ) { attr[ BPOS_STYLE ] &= ~ DIM ; } if ( object.italic === true ) { attr[ BPOS_STYLE ] |= ITALIC ; } else if ( object.italic === false ) { attr[ BPOS_STYLE ] &= ~ ITALIC ; } if ( object.underline === true ) { attr[ BPOS_STYLE ] |= UNDERLINE ; } else if ( object.underline === false ) { attr[ BPOS_STYLE ] &= ~ UNDERLINE ; } if ( object.blink === true ) { attr[ BPOS_STYLE ] |= BLINK ; } else if ( object.blink === false ) { attr[ BPOS_STYLE ] &= ~ BLINK ; } if ( object.inverse === true ) { attr[ BPOS_STYLE ] |= INVERSE ; } else if ( object.inverse === false ) { attr[ BPOS_STYLE ] &= ~ INVERSE ; } if ( object.hidden === true ) { attr[ BPOS_STYLE ] |= HIDDEN ; } else if ( object.hidden === false ) { attr[ BPOS_STYLE ] &= ~ HIDDEN ; } if ( object.strike === true ) { attr[ BPOS_STYLE ] |= STRIKE ; } else if ( object.strike === false ) { attr[ BPOS_STYLE ] &= ~ STRIKE ; } return attr ; } ; ScreenBufferHD.prototype.attrAndObject = ScreenBufferHD.attrAndObject ; // Used by TextBuffer for selection ScreenBufferHD.attrInverse = ScreenBufferHD.prototype.attrInverse = attr => { attr[ BPOS_STYLE ] ^= INVERSE ; return attr ; } ; /* Constants */ // General purpose flags const NONE = 0 ; // Nothing // Attr byte positions const BPOS_R = 0 ; const BPOS_G = 1 ; const BPOS_B = 2 ; const BPOS_A = 3 ; const BPOS_BG_R = 4 ; const BPOS_BG_G = 5 ; const BPOS_BG_B = 6 ; const BPOS_BG_A = 7 ; const BPOS_STYLE = 8 ; const BPOS_MISC = 9 ; const BPOS_FG = 0 ; const BPOS_BG = 4 ; // Style flags const BOLD = 1 ; const DIM = 2 ; const ITALIC = 4 ; const UNDERLINE = 8 ; const BLINK = 16 ; const INVERSE = 32 ; const HIDDEN = 64 ; const STRIKE = 128 ; const BOLD_DIM = BOLD | DIM ; // Misc flags const STYLE_TRANSPARENCY = 4 ; const CHAR_TRANSPARENCY = 8 ; // Special color: default terminal color //const FG_DEFAULT_COLOR = 16 ; //const BG_DEFAULT_COLOR = 32 ; const MISC_ATTR_MASK = STYLE_TRANSPARENCY | CHAR_TRANSPARENCY ; // | FG_DEFAULT_COLOR | BG_DEFAULT_COLOR ; // E.g.: if it needs redraw // Was never implemented, could be replaced by a full-transparency check //const VOID = 32 ; const LEADING_FULLWIDTH = 64 ; const TRAILING_FULLWIDTH = 128 ; const FULLWIDTH = LEADING_FULLWIDTH | TRAILING_FULLWIDTH ; const REMOVE_FULLWIDTH_FLAG = 255 ^ FULLWIDTH ; // Unused bits: 1, 2, 16, 32 // Tuning const OUTPUT_THRESHOLD = 10000 ; // minimum amount of data to retain before sending them to the terminal /* Cell structure: - 4 bytes: fg rgba - 4 bytes: bg rgba - 1 byte: style - 1 byte: misc/blending flags */ // Data structure ScreenBufferHD.prototype.ATTR_SIZE = 10 ; ScreenBufferHD.prototype.CHAR_SIZE = 4 ; ScreenBufferHD.prototype.ITEM_SIZE = ScreenBufferHD.prototype.ATTR_SIZE + ScreenBufferHD.prototype.CHAR_SIZE ; ScreenBufferHD.DEFAULT_ATTR = // <- used by TextBuffer ScreenBufferHD.prototype.DEFAULT_ATTR = ScreenBufferHD.object2attr( { color: { r: 255 , g: 255 , b: 255 , a: 255 } , bgColor: { r: 0 , g: 0 , b: 0 , a: 255 } } ) ; ScreenBufferHD.prototype.CLEAR_ATTR = ScreenBufferHD.object2attr( { color: { r: 255 , g: 255 , b: 255 , a: 0 } , bgColor: { r: 0 , g: 0 , b: 0 , a: 0 } , charTransparency: true , styleTransparency: true } ) ; ScreenBufferHD.prototype.CLEAR_BUFFER = Buffer.allocUnsafe( ScreenBufferHD.prototype.ITEM_SIZE ) ; ScreenBufferHD.prototype.CLEAR_ATTR.copy( ScreenBufferHD.prototype.CLEAR_BUFFER ) ; ScreenBufferHD.prototype.CLEAR_BUFFER.write( ' \x00\x00\x00' , ScreenBufferHD.prototype.ATTR_SIZE ) ; // space ScreenBufferHD.prototype.LEADING_FULLWIDTH = LEADING_FULLWIDTH ; ScreenBufferHD.prototype.TRAILING_FULLWIDTH = TRAILING_FULLWIDTH ; // Loader/Saver, mostly obsolete ScreenBufferHD.loadSyncV2 = function( filepath ) { var i , content , header , screenBuffer ; // Let it crash if nothing found content = fs.readFileSync( filepath ) ; // See if we have got a 'SB' at the begining of the file if ( content.length < 3 || content.toString( 'ascii' , 0 , 3 ) !== 'SB\n' ) { throw new Error( 'Magic number mismatch: this is not a ScreenBufferHD file' ) ; } // search for the second \n for ( i = 3 ; i < content.length ; i ++ ) { if ( content[ i ] === 0x0a ) { break ; } } if ( i === content.length ) { throw new Error( 'No header found: this is not a ScreenBufferHD file' ) ; } // Try to parse a JSON header try { header = JSON.parse( content.toString( 'utf8' , 3 , i ) ) ; } catch( error ) { throw new Error( 'No correct one-lined JSON header found: this is not a ScreenBufferHD file' ) ; } // Mandatory header field if ( header.version === undefined || header.width === undefined || header.height === undefined ) { throw new Error( 'Missing mandatory header data, this is a corrupted or obsolete ScreenBufferHD file' ) ; } // Check bitsPerColor if ( header.bitsPerColor && header.bitsPerColor !== ScreenBufferHD.prototype.bitsPerColor ) { throw new Error( 'Bad Bits Per Color: ' + header.bitsPerColor + ' (should be ' + ScreenBufferHD.prototype.bitsPerColor + ')' ) ; } // Bad size? if ( content.length !== i + 1 + header.width * header.height * ScreenBufferHD.prototype.ITEM_SIZE ) { throw new Error( 'Bad file size: this is a corrupted ScreenBufferHD file' ) ; } // So the file exists, create a canvas based upon it screenBuffer = new ScreenBufferHD( { width: header.width , height: header.height } ) ; content.copy( screenBuffer.buffer , 0 , i + 1 ) ; return screenBuffer ; } ; // This new format use JSON header for a maximal flexibility rather than a fixed binary header. // The header start with a magic number SB\n then a compact single-line JSON that end with an \n. // So the data part start after the second \n, providing a variable header size. // This will allow adding meta data without actually changing the file format. ScreenBufferHD.prototype.saveSyncV2 = function( filepath ) { var content , header ; header = { version: 2 , width: this.width , height: this.height } ; header = 'SB\n' + JSON.stringify( header ) + '\n' ; content = Buffer.allocUnsafe( header.length + this.buffer.length ) ; content.write( header ) ; this.buffer.copy( content , header.length ) ; // Let it crash if something bad happens fs.writeFileSync( filepath , content ) ; } ; ScreenBufferHD.loadSync = ScreenBufferHD.loadSyncV2 ; ScreenBufferHD.prototype.saveSync = ScreenBufferHD.prototype.saveSyncV2 ;