diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index dc375348..186cb2c1 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { createProgram, PROJECTION_MATRIX } from './WebglUtils'; +import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; import { INDICIES_PER_CELL } from './WebglRenderer'; @@ -75,7 +75,7 @@ const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; export class GlyphRenderer { - private _atlas: WebglCharAtlas; + private _atlas: WebglCharAtlas | undefined; private _program: WebGLProgram; private _vertexArrayObject: IWebGLVertexArrayObject; @@ -104,12 +104,16 @@ export class GlyphRenderer { ) { const gl = this._gl; - this._program = createProgram(gl, vertexShaderSource, fragmentShaderSource); + const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); + if (program === undefined) { + throw new Error('Could not create WebGL program'); + } + this._program = program; // Uniform locations - this._projectionLocation = gl.getUniformLocation(this._program, 'u_projection'); - this._resolutionLocation = gl.getUniformLocation(this._program, 'u_resolution'); - this._textureLocation = gl.getUniformLocation(this._program, 'u_texture'); + this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection')); + this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution')); + this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture')); // Create and set the vertex array object this._vertexArrayObject = gl.createVertexArray(); @@ -131,7 +135,7 @@ export class GlyphRenderer { gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); // Setup attributes - this._attributesBuffer = gl.createBuffer(); + this._attributesBuffer = throwIfFalsy(gl.createBuffer()); gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.enableVertexAttribArray(VertexAttribLocations.OFFSET); gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0); @@ -150,7 +154,7 @@ export class GlyphRenderer { gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1); // Setup empty texture atlas - this._atlasTexture = gl.createTexture(); + this._atlasTexture = throwIfFalsy(gl.createTexture()); gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255])); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); @@ -165,7 +169,7 @@ export class GlyphRenderer { } public beginFrame(): boolean { - return this._atlas.beginFrame(); + return this._atlas ? this._atlas.beginFrame() : true; } public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void { @@ -184,6 +188,9 @@ export class GlyphRenderer { } let rasterizedGlyph: IRasterizedGlyph; + if (!this._atlas) { + throw new Error('atlas must be set before updating cell'); + } if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg); } else { @@ -264,7 +271,7 @@ export class GlyphRenderer { if (!line) { line = terminal.buffer.getLine(row); } - const chars = line.getCell(x).char; + const chars = line!.getCell(x)!.char; this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars); } else { this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg); diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index d4ce3b4c..2c464d2a 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; +import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; import { fill } from 'common/TypedArrayUtils'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; @@ -66,8 +66,8 @@ export class RectangleRenderer { private _resolutionLocation: WebGLUniformLocation; private _attributesBuffer: WebGLBuffer; private _projectionLocation: WebGLUniformLocation; - private _bgFloat: Float32Array; - private _selectionFloat: Float32Array; + private _bgFloat!: Float32Array; + private _selectionFloat!: Float32Array; private _vertices: IVertices = { count: 0, @@ -83,11 +83,11 @@ export class RectangleRenderer { ) { const gl = this._gl; - this._program = createProgram(gl, vertexShaderSource, fragmentShaderSource); + this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); // Uniform locations - this._resolutionLocation = gl.getUniformLocation(this._program, 'u_resolution'); - this._projectionLocation = gl.getUniformLocation(this._program, 'u_projection'); + this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution')); + this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection')); // Create and set the vertex array object this._vertexArrayObject = gl.createVertexArray(); @@ -109,7 +109,7 @@ export class RectangleRenderer { gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); // Setup attributes - this._attributesBuffer = gl.createBuffer(); + this._attributesBuffer = throwIfFalsy(gl.createBuffer()); gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.enableVertexAttribArray(VertexAttribLocations.POSITION); gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index defed962..3d41e1b5 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -27,7 +27,7 @@ export const INDICIES_PER_CELL = 4; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; - private _charAtlas: WebglCharAtlas; + private _charAtlas: WebglCharAtlas | undefined; private _devicePixelRatio: number; private _model: RenderModel = new RenderModel(); @@ -57,18 +57,18 @@ export class WebglRenderer extends Disposable implements IRenderer { new CursorRenderLayer(this._core.screenElement, 3, this._colors) ]; this.dimensions = { - scaledCharWidth: null, - scaledCharHeight: null, - scaledCellWidth: null, - scaledCellHeight: null, - scaledCharLeft: null, - scaledCharTop: null, - scaledCanvasWidth: null, - scaledCanvasHeight: null, - canvasWidth: null, - canvasHeight: null, - actualCellWidth: null, - actualCellHeight: null + scaledCharWidth: 0, + scaledCharHeight: 0, + scaledCellWidth: 0, + scaledCellHeight: 0, + scaledCharLeft: 0, + scaledCharTop: 0, + scaledCanvasWidth: 0, + scaledCanvasHeight: 0, + canvasWidth: 0, + canvasHeight: 0, + actualCellWidth: 0, + actualCellHeight: 0 }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); @@ -252,7 +252,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (let y = start; y <= end; y++) { const row = y + terminal.buffer.ydisp; - const line = terminal.buffer.lines.get(row); + const line = terminal.buffer.lines.get(row)!; this._model.lineLengths[y] = 0; for (let x = 0; x < terminal.cols; x++) { const charData = line.get(x); diff --git a/addons/xterm-addon-webgl/src/WebglUtils.ts b/addons/xterm-addon-webgl/src/WebglUtils.ts index 8f166a23..ff62388e 100644 --- a/addons/xterm-addon-webgl/src/WebglUtils.ts +++ b/addons/xterm-addon-webgl/src/WebglUtils.ts @@ -15,9 +15,9 @@ export const PROJECTION_MATRIX = new Float32Array([ ]); export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram | undefined { - const program = gl.createProgram(); - gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexSource)); - gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)); + const program = throwIfFalsy(gl.createProgram()); + gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource))); + gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource))); gl.linkProgram(program); const success = gl.getProgramParameter(program, gl.LINK_STATUS); if (success) { @@ -29,7 +29,7 @@ export function createProgram(gl: WebGLRenderingContext, vertexSource: string, f } export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined { - const shader = gl.createShader(type); + const shader = throwIfFalsy(gl.createShader(type)); gl.shaderSource(shader, source); gl.compileShader(shader); const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); @@ -49,3 +49,10 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar } return newArray; } + +export function throwIfFalsy(value: T | undefined | null): T { + if (!value) { + throw new Error('value must not be falsy'); + } + return value; +} diff --git a/addons/xterm-addon-webgl/src/atlas/BaseCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/BaseCharAtlas.ts deleted file mode 100644 index 470736f1..00000000 --- a/addons/xterm-addon-webgl/src/atlas/BaseCharAtlas.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IGlyphIdentifier } from './Types'; -import { IDisposable } from 'xterm'; - -export abstract class BaseCharAtlas implements IDisposable { - private _didWarmUp: boolean = false; - - public dispose(): void { } - - /** - * Perform any work needed to warm the cache before it can be used. May be called multiple times. - * Implement _doWarmUp instead if you only want to get called once. - */ - public warmUp(): void { - if (!this._didWarmUp) { - this._doWarmUp(); - this._didWarmUp = true; - } - } - - /** - * Perform any work needed to warm the cache before it can be used. Used by the default - * implementation of warmUp(), and will only be called once. - */ - protected _doWarmUp(): void { } - - /** - * Called when we start drawing a new frame. - * - * TODO: We rely on this getting called by TextRenderLayer. This should really be called by - * Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead - * of BaseRenderLayer. - */ - public beginFrame(): void { } - - /** - * May be called before warmUp finishes, however it is okay for the implementation to - * do nothing and return false in that case. - * - * @param ctx Where to draw the character onto. - * @param glyph Information about what to draw - * @param x The position on the context to start drawing at - * @param y The position on the context to start drawing at - * @returns The success state. True if we drew the character. - */ - public abstract draw( - ctx: CanvasRenderingContext2D, - glyph: IGlyphIdentifier, - x: number, - y: number - ): boolean; -} diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts index 647b96b4..5046006f 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts @@ -4,14 +4,13 @@ */ import { generateConfig, configEquals } from './CharAtlasUtils'; -import { BaseCharAtlas } from './BaseCharAtlas'; import { WebglCharAtlas } from './WebglCharAtlas'; import { ICharAtlasConfig } from './Types'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; interface ICharAtlasCacheEntry { - atlas: BaseCharAtlas; + atlas: WebglCharAtlas; config: ICharAtlasConfig; // N.B. This implementation potentially holds onto copies of the terminal forever, so // this may cause memory leaks. @@ -31,7 +30,7 @@ export function acquireCharAtlas( colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number -): BaseCharAtlas { +): WebglCharAtlas { const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); // Check to see if the terminal already owns this config diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 1c43dd70..857fa628 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -6,16 +6,21 @@ import { ICharAtlasConfig } from './Types'; import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { Terminal, FontWeight } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IColor } from 'browser/Types'; + +const NULL_COLOR: IColor = { + css: '', + rgba: 0 +}; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IColorSet = { foreground: colors.foreground, background: colors.background, - cursor: null, - cursorAccent: null, - selection: null, + cursor: NULL_COLOR, + cursorAccent: NULL_COLOR, + selection: NULL_COLOR, // For the static char atlas, we only use the first 16 colors, but we need all 256 for the // dynamic character atlas. ansi: colors.ansi.slice() diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 6a941d55..8d8e995b 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -3,14 +3,15 @@ * @license MIT */ -import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; +import { ICharAtlasConfig } from './Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { BaseCharAtlas } from './BaseCharAtlas'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; import { DEFAULT_COLOR, DEFAULT_ATTR } from 'common/buffer/Constants'; import { is256Color } from './CharAtlasUtils'; +import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'browser/Types'; import { FLAGS } from '../Constants'; +import { IDisposable } from 'xterm'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. @@ -42,7 +43,9 @@ const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = { const TMP_CANVAS_GLYPH_PADDING = 2; -export class WebglCharAtlas extends BaseCharAtlas { +export class WebglCharAtlas implements IDisposable { + private _didWarmUp: boolean = false; + private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {}; private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {}; @@ -67,20 +70,18 @@ export class WebglCharAtlas extends BaseCharAtlas { private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; constructor(document: Document, private _config: ICharAtlasConfig) { - super(); - this.cacheCanvas = document.createElement('canvas'); this.cacheCanvas.width = TEXTURE_WIDTH; this.cacheCanvas.height = TEXTURE_HEIGHT; // The canvas needs alpha because we use clearColor to convert the background color to alpha. // It might also contain some characters with transparent backgrounds if allowTransparency is // set. - this._cacheCtx = this.cacheCanvas.getContext('2d', {alpha: true}); + this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', {alpha: true})); this._tmpCanvas = document.createElement('canvas'); this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2; - this._tmpCtx = this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}); + this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency})); // This is useful for debugging document.body.appendChild(this.cacheCanvas); @@ -92,6 +93,13 @@ export class WebglCharAtlas extends BaseCharAtlas { } } + public warmUp(): void { + if (!this._didWarmUp) { + this._doWarmUp(); + this._didWarmUp = true; + } + } + protected _doWarmUp(): void { // Pre-fill with ASCII 33-126 for (let i = 33; i < 126; i++) { @@ -146,15 +154,6 @@ export class WebglCharAtlas extends BaseCharAtlas { return rasterizedGlyph; } - public draw( - ctx: CanvasRenderingContext2D, - glyph: IGlyphIdentifier, - x: number, - y: number - ): boolean { - throw new Error('WebglCharAtlas is only compatible with the webgl renderer'); - } - private _getColorFromAnsiIndex(idx: number): IColor { if (idx >= this._config.colors.ansi.length) { throw new Error('No color found for idx ' + idx); diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 0422440e..df0cdbab 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -8,17 +8,18 @@ import { ICellData } from 'common/Types'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IGlyphIdentifier } from '../atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { BaseCharAtlas } from '../atlas/BaseCharAtlas'; import { acquireCharAtlas } from '../atlas/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; +import { WebglCharAtlas } from 'atlas/WebglCharAtlas'; +import { throwIfFalsy } from '../WebglUtils'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; - protected _ctx: CanvasRenderingContext2D; + protected _ctx!: CanvasRenderingContext2D; private _scaledCharWidth: number = 0; private _scaledCharHeight: number = 0; private _scaledCellWidth: number = 0; @@ -26,7 +27,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - protected _charAtlas: BaseCharAtlas; + protected _charAtlas: WebglCharAtlas | undefined; /** * An object that's reused when drawing glyphs in order to reduce GC. @@ -63,7 +64,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } private _initCanvas(): void { - this._ctx = this._canvas.getContext('2d', {alpha: this._alpha}); + this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha})); // Draw the background if this is an opaque layer if (!this._alpha) { this._clearAll(); @@ -249,115 +250,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } - /** - * Draws one or more characters at a cell. If possible this will draw using - * the character atlas to reduce draw time. - * @param terminal The terminal. - * @param chars The character or characters. - * @param code The character code. - * @param width The width of the characters. - * @param x The column to draw at. - * @param y The row to draw at. - * @param fg The foreground color, in the format stored within the attributes. - * @param bg The background color, in the format stored within the attributes. - * This is used to validate whether a cached image can be used. - * @param bold Whether the text is bold. - */ - protected _drawChars(terminal: Terminal, cell: ICellData, x: number, y: number): void { - - // skip cache right away if we draw in RGB - // Note: to avoid bad runtime JoinedCellData will be skipped - // in the cache handler itself (atlasDidDraw == false) and - // fall through to uncached later down below - if (cell.isFgRGB() || cell.isBgRGB()) { - this._drawUncachedChars(terminal, cell, x, y); - return; - } - - let fg; - let bg; - if (cell.isInverse()) { - fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor(); - bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor(); - } else { - bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor(); - fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); - } - - const drawInBrightColor = terminal.getOption('drawBoldTextInBrightColors') && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; - - fg += drawInBrightColor ? 8 : 0; - this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; - this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE; - this._currentGlyphIdentifier.bg = bg; - this._currentGlyphIdentifier.fg = fg; - this._currentGlyphIdentifier.bold = !!cell.isBold(); - this._currentGlyphIdentifier.dim = !!cell.isDim(); - this._currentGlyphIdentifier.italic = !!cell.isItalic(); - const atlasDidDraw = this._charAtlas && this._charAtlas.draw( - this._ctx, - this._currentGlyphIdentifier, - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop - ); - - if (!atlasDidDraw) { - this._drawUncachedChars(terminal, cell, x, y); - } - } - - /** - * Draws one or more characters at one or more cells. The character(s) will be - * clipped to ensure that they fit with the cell(s), including the cell to the - * right if the last character is a wide character. - * @param terminal The terminal. - * @param chars The character. - * @param width The width of the character. - * @param fg The foreground color, in the format stored within the attributes. - * @param x The column to draw at. - * @param y The row to draw at. - */ - private _drawUncachedChars(terminal: Terminal, cell: ICellData, x: number, y: number): void { - this._ctx.save(); - this._ctx.font = this._getFont(terminal, !!cell.isBold(), !!cell.isItalic()); - this._ctx.textBaseline = 'middle'; - - if (cell.isInverse()) { - if (cell.isBgDefault()) { - this._ctx.fillStyle = this._colors.background.css; - } else if (cell.isBgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; - } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; - } - } else { - if (cell.isFgDefault()) { - this._ctx.fillStyle = this._colors.foreground.css; - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else { - let fg = cell.getFgColor(); - if (terminal.getOption('drawBoldTextInBrightColors') && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - - this._clipRow(terminal, y); - - // Apply alpha to dim the character - if (cell.isDim()) { - this._ctx.globalAlpha = DIM_OPACITY; - } - // Draw the character - this._ctx.fillText( - cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); - this._ctx.restore(); - } - /** * Clips a row to ensure no pixels will be drawn outside the cells in the row. * @param terminal The terminal. diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 09abe82a..6aed5f53 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -26,17 +26,17 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; private _cursorRenderers: {[key: string]: (terminal: Terminal, x: number, y: number, cell: ICellData) => void}; - private _cursorBlinkStateManager: CursorBlinkStateManager; + private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _cell: ICellData = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'cursor', zIndex, true, colors); this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), @@ -50,11 +50,11 @@ export class CursorRenderLayer extends BaseRenderLayer { super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; } @@ -62,7 +62,6 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.dispose(); - this._cursorBlinkStateManager = null; this.onOptionsChanged(terminal); } } @@ -92,7 +91,6 @@ export class CursorRenderLayer extends BaseRenderLayer { } else { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.dispose(); - this._cursorBlinkStateManager = null; } // Request a refresh from the terminal as management of rendering is being // moved back to the terminal @@ -184,11 +182,11 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._state) { this._clearCells(this._state.x, this._state.y, this._state.width, 1); this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; } } @@ -227,16 +225,16 @@ export class CursorRenderLayer extends BaseRenderLayer { class CursorBlinkStateManager { public isCursorVisible: boolean; - private _animationFrame: number; - private _blinkStartTimeout: number; - private _blinkInterval: number; + private _animationFrame: number | undefined; + private _blinkStartTimeout: number | undefined; + private _blinkInterval: number | undefined; /** * The time at which the animation frame was restarted, this is used on the * next render to restart the timers so they don't need to restart the timers * multiple times over a short period. */ - private _animationTimeRestarted: number; + private _animationTimeRestarted: number | undefined; constructor( terminal: Terminal, @@ -253,15 +251,15 @@ class CursorBlinkStateManager { public dispose(): void { if (this._blinkInterval) { window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + this._blinkInterval = undefined; } if (this._blinkStartTimeout) { window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = null; + this._blinkStartTimeout = undefined; } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } @@ -276,7 +274,7 @@ class CursorBlinkStateManager { if (!this._animationFrame) { this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); } } @@ -296,7 +294,7 @@ class CursorBlinkStateManager { // started if (this._animationTimeRestarted) { const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; if (time > 0) { this._restartInterval(time); return; @@ -307,7 +305,7 @@ class CursorBlinkStateManager { this.isCursorVisible = false; this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); // Setup the blink interval @@ -317,7 +315,7 @@ class CursorBlinkStateManager { // calc time diff // Make restart interval do a setTimeout initially? const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; this._restartInterval(time); return; } @@ -326,7 +324,7 @@ class CursorBlinkStateManager { this.isCursorVisible = !this.isCursorVisible; this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); }, BLINK_INTERVAL); }, timeToStart); @@ -336,20 +334,20 @@ class CursorBlinkStateManager { this.isCursorVisible = true; if (this._blinkInterval) { window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + this._blinkInterval = undefined; } if (this._blinkStartTimeout) { window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = null; + this._blinkStartTimeout = undefined; } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } public resume(terminal: Terminal): void { - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; this._restartInterval(); this.restartBlinkAnimation(terminal); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 7c79ddcc..a29d2cfd 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -12,7 +12,7 @@ import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; export class LinkRenderLayer extends BaseRenderLayer { - private _state: ILinkifierEvent = null; + private _state: ILinkifierEvent | undefined; constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { super(container, 'link', zIndex, true, colors); @@ -23,7 +23,7 @@ export class LinkRenderLayer extends BaseRenderLayer { public resize(terminal: Terminal, dim: IRenderDimensions): void { super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state - this._state = null; + this._state = undefined; } public reset(terminal: Terminal): void { @@ -38,7 +38,7 @@ export class LinkRenderLayer extends BaseRenderLayer { this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount); } this._clearCells(0, this._state.y2, this._state.x2, 1); - this._state = null; + this._state = undefined; } } diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index 34149159..00134015 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -14,7 +14,8 @@ "paths": { "common/*": [ "../../../src/common/*" ], "browser/*": [ "../../../src/browser/*" ] - } + }, + "strict": true }, "include": [ "./**/*", diff --git a/demo/server.js b/demo/server.js index 3b170813..8955431b 100644 --- a/demo/server.js +++ b/demo/server.js @@ -37,14 +37,16 @@ function startServer() { }); app.post('/terminals', function (req, res) { + const env = Object.assign({}, process.env); + env['COLORTERM'] = 'truecolor'; var cols = parseInt(req.query.cols), rows = parseInt(req.query.rows), term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { name: 'xterm-256color', cols: cols || 80, rows: rows || 24, - cwd: process.env.PWD, - env: process.env, + cwd: env.PWD, + env: env, encoding: USE_BINARY_UTF8 ? null : 'utf8' }); diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 553997cd..a39ad9b9 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -12,6 +12,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; +import { Params } from 'common/parser/Params'; import { MockCoreService } from 'common/TestUtils.test'; describe('InputHandler', () => { @@ -23,7 +24,7 @@ describe('InputHandler', () => { terminal.curAttrData.fg = 3; const inputHandler = new InputHandler(terminal, new MockCoreService()); // Save cursor position - inputHandler.saveCursor([]); + inputHandler.saveCursor(); assert.equal(terminal.buffer.x, 1); assert.equal(terminal.buffer.y, 2); assert.equal(terminal.curAttrData.fg, 3); @@ -32,7 +33,7 @@ describe('InputHandler', () => { terminal.buffer.y = 20; terminal.curAttrData.fg = 30; // Restore cursor position - inputHandler.restoreCursor([]); + inputHandler.restoreCursor(); assert.equal(terminal.buffer.x, 1); assert.equal(terminal.buffer.y, 2); assert.equal(terminal.curAttrData.fg, 3); @@ -43,37 +44,37 @@ describe('InputHandler', () => { const inputHandler = new InputHandler(terminal, new MockCoreService()); const collect = ' '; - inputHandler.setCursorStyle([0], collect); + inputHandler.setCursorStyle(Params.fromArray([0]), collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([1], collect); + inputHandler.setCursorStyle(Params.fromArray([1]), collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([2], collect); + inputHandler.setCursorStyle(Params.fromArray([2]), collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], false); terminal.options = {}; - inputHandler.setCursorStyle([3], collect); + inputHandler.setCursorStyle(Params.fromArray([3]), collect); assert.equal(terminal.options['cursorStyle'], 'underline'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([4], collect); + inputHandler.setCursorStyle(Params.fromArray([4]), collect); assert.equal(terminal.options['cursorStyle'], 'underline'); assert.equal(terminal.options['cursorBlink'], false); terminal.options = {}; - inputHandler.setCursorStyle([5], collect); + inputHandler.setCursorStyle(Params.fromArray([5]), collect); assert.equal(terminal.options['cursorStyle'], 'bar'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([6], collect); + inputHandler.setCursorStyle(Params.fromArray([6]), collect); assert.equal(terminal.options['cursorStyle'], 'bar'); assert.equal(terminal.options['cursorBlink'], false); }); @@ -85,10 +86,10 @@ describe('InputHandler', () => { terminal.bracketedPasteMode = false; const inputHandler = new InputHandler(terminal, new MockCoreService()); // Set bracketed paste mode - inputHandler.setMode([2004], collect); + inputHandler.setMode(Params.fromArray([2004]), collect); assert.equal(terminal.bracketedPasteMode, true); // Reset bracketed paste mode - inputHandler.resetMode([2004], collect); + inputHandler.resetMode(Params.fromArray([2004]), collect); assert.equal(terminal.bracketedPasteMode, false); }); }); @@ -114,25 +115,25 @@ describe('InputHandler', () => { // insert one char from params = [0] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.insertChars([0]); + inputHandler.insertChars(Params.fromArray([0])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789'); // insert one char from params = [1] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.insertChars([1]); + inputHandler.insertChars(Params.fromArray([1])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678'); // insert two chars from params = [2] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.insertChars([2]); + inputHandler.insertChars(Params.fromArray([2])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456'); // insert 10 chars from params = [10] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.insertChars([10]); + inputHandler.insertChars(Params.fromArray([10])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' '); expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a')); }); @@ -151,28 +152,28 @@ describe('InputHandler', () => { // delete one char from params = [0] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.deleteChars([0]); + inputHandler.deleteChars(Params.fromArray([0])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 '); expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890'); // insert one char from params = [1] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.deleteChars([1]); + inputHandler.deleteChars(Params.fromArray([1])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 '); expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890'); // insert two chars from params = [2] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.deleteChars([2]); + inputHandler.deleteChars(Params.fromArray([2])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 '); expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890'); // insert 10 chars from params = [10] term.buffer.y = 0; term.buffer.x = 70; - inputHandler.deleteChars([10]); + inputHandler.deleteChars(Params.fromArray([10])); expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' '); expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a')); }); @@ -188,19 +189,19 @@ describe('InputHandler', () => { // params[0] - right erase term.buffer.y = 0; term.buffer.x = 70; - inputHandler.eraseInLine([0]); + inputHandler.eraseInLine(Params.fromArray([0])); expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' '); // params[1] - left erase term.buffer.y = 1; term.buffer.x = 70; - inputHandler.eraseInLine([1]); + inputHandler.eraseInLine(Params.fromArray([1])); expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa'); // params[1] - left erase term.buffer.y = 2; term.buffer.x = 70; - inputHandler.eraseInLine([2]); + inputHandler.eraseInLine(Params.fromArray([2])); expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' ')); }); @@ -214,7 +215,7 @@ describe('InputHandler', () => { // params [0] - right and below erase term.buffer.y = 5; term.buffer.x = 40; - inputHandler.eraseInDisplay([0]); + inputHandler.eraseInDisplay(Params.fromArray([0])); expect(termContent(term, false)).eql([ Array(term.cols + 1).join('a'), Array(term.cols + 1).join('a'), @@ -242,7 +243,7 @@ describe('InputHandler', () => { // params [1] - left and above term.buffer.y = 5; term.buffer.x = 40; - inputHandler.eraseInDisplay([1]); + inputHandler.eraseInDisplay(Params.fromArray([1])); expect(termContent(term, false)).eql([ Array(term.cols + 1).join(' '), Array(term.cols + 1).join(' '), @@ -270,7 +271,7 @@ describe('InputHandler', () => { // params [2] - whole screen term.buffer.y = 5; term.buffer.x = 40; - inputHandler.eraseInDisplay([2]); + inputHandler.eraseInDisplay(Params.fromArray([2])); expect(termContent(term, false)).eql([ Array(term.cols + 1).join(' '), Array(term.cols + 1).join(' '), @@ -302,7 +303,7 @@ describe('InputHandler', () => { expect(term.buffer.lines.get(2).isWrapped).true; term.buffer.y = 2; term.buffer.x = 40; - inputHandler.eraseInDisplay([1]); + inputHandler.eraseInDisplay(Params.fromArray([1])); expect(term.buffer.lines.get(2).isWrapped).false; // reset and add a wrapped line @@ -317,7 +318,7 @@ describe('InputHandler', () => { expect(term.buffer.lines.get(2).isWrapped).true; term.buffer.y = 1; term.buffer.x = 90; // Cursor is beyond last column - inputHandler.eraseInDisplay([1]); + inputHandler.eraseInDisplay(Params.fromArray([1])); expect(term.buffer.lines.get(2).isWrapped).false; }); }); @@ -546,6 +547,156 @@ describe('InputHandler', () => { assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]); }); }); + describe('colon notation', () => { + let termColon: TestTerminal; + let termSemicolon: TestTerminal; + beforeEach(() => { + termColon = new TestTerminal(); + termSemicolon = new TestTerminal(); + }); + describe('should equal to semicolon', () => { + it('CSI 38:2::50:100:150 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;50;100;150m'); + termColon.writeSync('\x1b[38:2::50:100:150m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38:2::50:100: m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;50;100;m'); + termColon.writeSync('\x1b[38:2::50:100:m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38:2::50:: m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;50;;m'); + termColon.writeSync('\x1b[38:2::50::m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38:2:::: m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;;;m'); + termColon.writeSync('\x1b[38:2::::m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38;2::50:100:150 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;50;100;150m'); + termColon.writeSync('\x1b[38;2::50:100:150m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38;2;50:100:150 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;50;100;150m'); + termColon.writeSync('\x1b[38;2;50:100:150m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38;2;50;100:150 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2;50;100;150m'); + termColon.writeSync('\x1b[38;2;50;100:150m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38:5:50 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;5;50m'); + termColon.writeSync('\x1b[38:5:50m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFF, 50); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38:5: m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;5;m'); + termColon.writeSync('\x1b[38:5:m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFF, 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38;5:50 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;5;50m'); + termColon.writeSync('\x1b[38;5:50m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFF, 50); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + }); + describe('should fill early sequence end with default of 0', () => { + it('CSI 38:2 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;2m'); + termColon.writeSync('\x1b[38:2m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 38:5 m', () => { + termColon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.curAttrData.fg = 0xFFFFFFFF; + termSemicolon.writeSync('\x1b[38;5m'); + termColon.writeSync('\x1b[38:5m'); + assert.equal(termSemicolon.curAttrData.fg & 0xFF, 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + }); + describe('should not interfere with leading/following SGR attrs', () => { + it('CSI 1 ; 38:2::50:100:150 ; 4 m', () => { + termSemicolon.writeSync('\x1b[1;38;2;50;100;150;4m'); + termColon.writeSync('\x1b[1;38:2::50:100:150;4m'); + assert.equal(!!termSemicolon.curAttrData.isBold(), true); + assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 1 ; 38:2::50:100: ; 4 m', () => { + termSemicolon.writeSync('\x1b[1;38;2;50;100;;4m'); + termColon.writeSync('\x1b[1;38:2::50:100:;4m'); + assert.equal(!!termSemicolon.curAttrData.isBold(), true); + assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 1 ; 38:2::50:100 ; 4 m', () => { + termSemicolon.writeSync('\x1b[1;38;2;50;100;;4m'); + termColon.writeSync('\x1b[1;38:2::50:100;4m'); + assert.equal(!!termSemicolon.curAttrData.isBold(), true); + assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 1 ; 38:2:: ; 4 m', () => { + termSemicolon.writeSync('\x1b[1;38;2;;;;4m'); + termColon.writeSync('\x1b[1;38:2::;4m'); + assert.equal(!!termSemicolon.curAttrData.isBold(), true); + assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + it('CSI 1 ; 38;2:: ; 4 m', () => { + termSemicolon.writeSync('\x1b[1;38;2;;;;4m'); + termColon.writeSync('\x1b[1;38;2::;4m'); + assert.equal(!!termSemicolon.curAttrData.isBold(), true); + assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); + assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0); + assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); + }); + }); + }); describe('cursor positioning', () => { let term: TestTerminal; beforeEach(() => { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1ac02944..b6a82150 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,13 +14,13 @@ import { concat } from 'common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; +import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; -import { IDisposable } from 'common/Types'; /** * Map collect to glevel. Used in `selectCharset`. @@ -43,7 +43,7 @@ class DECRQSS implements IDcsHandler { constructor(private _terminal: any) { } - hook(collect: string, params: number[], flag: number): void { + hook(collect: string, params: IParams, flag: number): void { this._data = new Uint32Array(0); } @@ -134,8 +134,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * custom fallback handlers */ - this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { - this._terminal.error('Unknown CSI code: ', { collect, params, flag: String.fromCharCode(flag) }); + this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { + this._terminal.error('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) }); }); this._parser.setEscHandlerFallback((collect: string, flag: number) => { this._terminal.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) }); @@ -255,8 +255,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * ESC handlers */ - this._parser.setEscHandler('7', () => this.saveCursor([])); - this._parser.setEscHandler('8', () => this.restoreCursor([])); + this._parser.setEscHandler('7', () => this.saveCursor()); + this._parser.setEscHandler('8', () => this.restoreCursor()); this._parser.setEscHandler('D', () => this.index()); this._parser.setEscHandler('E', () => this.nextLine()); this._parser.setEscHandler('H', () => this.tabSet()); @@ -485,7 +485,7 @@ export class InputHandler extends Disposable implements IInputHandler { /** * Forward addCsiHandler from parser. */ - public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { return this._parser.addCsiHandler(flag, callback); } @@ -586,13 +586,13 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps @ * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ - public insertChars(params: number[]): void { + public insertChars(params: IParams): void { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, - params[0] || 1, + params.params[0] || 1, this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); @@ -635,48 +635,32 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps A * Cursor Up Ps Times (default = 1) (CUU). */ - public cursorUp(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(0, -param); + public cursorUp(params: IParams): void { + this._moveCursor(0, -(params.params[0] || 1)); } /** * CSI Ps B * Cursor Down Ps Times (default = 1) (CUD). */ - public cursorDown(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(0, param); + public cursorDown(params: IParams): void { + this._moveCursor(0, params.params[0] || 1); } /** * CSI Ps C * Cursor Forward Ps Times (default = 1) (CUF). */ - public cursorForward(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(param, 0); + public cursorForward(params: IParams): void { + this._moveCursor(params.params[0] || 1, 0); } /** * CSI Ps D * Cursor Backward Ps Times (default = 1) (CUB). */ - public cursorBackward(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(-param, 0); + public cursorBackward(params: IParams): void { + this._moveCursor(-(params.params[0] || 1), 0); } /** @@ -684,12 +668,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Next Line Ps Times (default = 1) (CNL). * Other than cursorDown (CUD) also set the cursor to first column. */ - public cursorNextLine(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(0, param); + public cursorNextLine(params: IParams): void { + this._moveCursor(0, params.params[0] || 1); this._terminal.buffer.x = 0; } @@ -699,12 +679,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Previous Line Ps Times (default = 1) (CPL). * Other than cursorUp (CUU) also set the cursor to first column. */ - public cursorPrecedingLine(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(0, -param); + public cursorPrecedingLine(params: IParams): void { + this._moveCursor(0, -(params.params[0] || 1)); this._terminal.buffer.x = 0; } @@ -713,39 +689,31 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps G * Cursor Character Absolute [column] (default = [row,1]) (CHA). */ - public cursorCharAbsolute(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._setCursor(param - 1, this._terminal.buffer.y); + public cursorCharAbsolute(params: IParams): void { + this._setCursor((params.params[0] || 1) - 1, this._terminal.buffer.y); } /** * CSI Ps ; Ps H * Cursor Position [row;column] (default = [1,1]) (CUP). */ - public cursorPosition(params: number[]): void { - let col: number; - const row: number = params[0] - 1; - - if (params.length >= 2) { - col = params[1] - 1; - } else { - col = 0; - } - this._setCursor(col, row); + public cursorPosition(params: IParams): void { + this._setCursor( + // col + (params.length >= 2) ? (params.params[1] || 1) - 1 : 0, + // row + (params.params[0] || 1) - 1); } /** * CSI Ps I * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). */ - public cursorForwardTab(params: number[]): void { + public cursorForwardTab(params: IParams): void { if (this._terminal.buffer.x >= this._terminal.cols) { return; } - let param = params[0] || 1; + let param = params.params[0] || 1; while (param--) { this._terminal.buffer.x = this._terminal.buffer.nextStop(); } @@ -791,9 +759,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase Above. * Ps = 2 -> Selective Erase All. */ - public eraseInDisplay(params: number[]): void { + public eraseInDisplay(params: IParams): void { let j; - switch (params[0]) { + switch (params.params[0]) { case 0: j = this._terminal.buffer.y; this._terminal.updateRange(j); @@ -850,11 +818,11 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase to Left. * Ps = 2 -> Selective Erase All. */ - public eraseInLine(params: number[]): void { + public eraseInLine(params: IParams): void { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } - switch (params[0]) { + switch (params.params[0]) { case 0: this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols); break; @@ -872,11 +840,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps L * Insert Ps Line(s) (default = 1) (IL). */ - public insertLines(params: number[]): void { - let param: number = params[0]; - if (param < 1) { - param = 1; - } + public insertLines(params: IParams): void { + let param = params.params[0] || 1; // make buffer local for faster access const buffer = this._terminal.buffer; @@ -902,11 +867,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps M * Delete Ps Line(s) (default = 1) (DL). */ - public deleteLines(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public deleteLines(params: IParams): void { + let param = params.params[0] || 1; // make buffer local for faster access const buffer = this._terminal.buffer; @@ -933,10 +895,10 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps P * Delete Ps Character(s) (default = 1) (DCH). */ - public deleteChars(params: number[]): void { + public deleteChars(params: IParams): void { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, - params[0] || 1, + params.params[0] || 1, this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); @@ -945,8 +907,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps S Scroll up Ps lines (default = 1) (SU). */ - public scrollUp(params: number[]): void { - let param = params[0] || 1; + public scrollUp(params: IParams): void { + let param = params.params[0] || 1; // make buffer local for faster access const buffer = this._terminal.buffer; @@ -963,9 +925,9 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: number[], collect?: string): void { + public scrollDown(params: IParams, collect?: string): void { if (params.length < 2 && !collect) { - let param = params[0] || 1; + let param = params.params[0] || 1; // make buffer local for faster access const buffer = this._terminal.buffer; @@ -984,10 +946,10 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps X * Erase Ps Character(s) (default = 1) (ECH). */ - public eraseChars(params: number[]): void { + public eraseChars(params: IParams): void { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, - this._terminal.buffer.x + (params[0] || 1), + this._terminal.buffer.x + (params.params[0] || 1), this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); @@ -996,8 +958,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). */ - public cursorBackwardTab(params: number[]): void { - let param = params[0] || 1; + public cursorBackwardTab(params: IParams): void { + let param = params.params[0] || 1; // make buffer local for faster access const buffer = this._terminal.buffer; @@ -1012,12 +974,8 @@ export class InputHandler extends Disposable implements IInputHandler { * [column] (default = [row,1]) (HPA). * Currently same functionality as CHA. */ - public charPosAbsolute(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._setCursor(param - 1, this._terminal.buffer.y); + public charPosAbsolute(params: IParams): void { + this._setCursor((params.params[0] || 1) - 1, this._terminal.buffer.y); } /** @@ -1025,12 +983,8 @@ export class InputHandler extends Disposable implements IInputHandler { * [columns] (default = [row,col+1]) (HPR) * Currently same functionality as CUF. */ - public hPositionRelative(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(param, 0); + public hPositionRelative(params: IParams): void { + this._moveCursor(params.params[0] || 1, 0); } /** @@ -1055,12 +1009,12 @@ export class InputHandler extends Disposable implements IInputHandler { * Note: To get reset on a valid sequence working correctly without much runtime penalty, * the preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`. */ - public repeatPrecedingCharacter(params: number[]): void { + public repeatPrecedingCharacter(params: IParams): void { if (!this._parser.precedingCodepoint) { return; } // call print to insert the chars and handle correct wrapping - const length = params[0] || 1; + const length = params.params[0] || 1; const data = new Uint32Array(length); for (let i = 0; i < length; ++i) { data[i] = this._parser.precedingCodepoint; @@ -1105,8 +1059,8 @@ export class InputHandler extends Disposable implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: number[], collect?: string): void { - if (params[0] > 0) { + public sendDeviceAttributes(params: IParams, collect?: string): void { + if (params.params[0] > 0) { return; } @@ -1127,7 +1081,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (this._terminal.is('linux')) { // not supported by linux console. // linux console echoes parameters. - this._coreService.triggerDataEvent(params[0] + 'c'); + this._coreService.triggerDataEvent(params.params[0] + 'c'); } else if (this._terminal.is('screen')) { this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } @@ -1138,12 +1092,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Pm d Vertical Position Absolute (VPA) * [row] (default = [1,column]) */ - public linePosAbsolute(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._setCursor(this._terminal.buffer.x, param - 1); + public linePosAbsolute(params: IParams): void { + this._setCursor(this._terminal.buffer.x, (params.params[0] || 1) - 1); } /** @@ -1151,12 +1101,8 @@ export class InputHandler extends Disposable implements IInputHandler { * [rows] (default = [row+1,column]) * reuse CSI Ps B ? */ - public vPositionRelative(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } - this._moveCursor(0, param); + public vPositionRelative(params: IParams): void { + this._moveCursor(0, params.params[0] || 1); } /** @@ -1165,7 +1111,7 @@ export class InputHandler extends Disposable implements IInputHandler { * [1,1]) (HVP). * Same as CUP. */ - public hVPosition(params: number[]): void { + public hVPosition(params: IParams): void { this.cursorPosition(params); } @@ -1177,9 +1123,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 2 -> Clear Stops on Line. * http://vt100.net/annarbor/aaa-ug/section6.html */ - public tabClear(params: number[]): void { - const param = params[0]; - if (param <= 0) { + public tabClear(params: IParams): void { + const param = params.params[0]; + if (param === 0) { delete this._terminal.buffer.tabs[this._terminal.buffer.x]; } else if (param === 3) { this._terminal.buffer.tabs = {}; @@ -1272,17 +1218,15 @@ export class InputHandler extends Disposable implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: number[], collect?: string): void { - if (params.length > 1) { - for (let i = 0; i < params.length; i++) { - this.setMode([params[i]]); - } - - return; + public setMode(params: IParams, collect?: string): void { + for (let i = 0; i < params.length; i++) { + this._setMode(params.params[i], collect); } + } + private _setMode(param: number, collect?: string): void { if (!collect) { - switch (params[0]) { + switch (param) { case 4: this._terminal.insertMode = true; break; @@ -1291,7 +1235,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } else if (collect === '?') { - switch (params[0]) { + switch (param) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = true; break; @@ -1334,9 +1278,9 @@ export class InputHandler extends Disposable implements IInputHandler { // TODO: Why are params[0] compares nested within a switch for params[0]? - this._terminal.x10Mouse = params[0] === 9; - this._terminal.vt200Mouse = params[0] === 1000; - this._terminal.normalMouse = params[0] > 1000; + this._terminal.x10Mouse = param === 9; + this._terminal.vt200Mouse = param === 1000; + this._terminal.normalMouse = param > 1000; this._terminal.mouseEvents = true; if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); @@ -1374,10 +1318,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.cursorHidden = false; break; case 1048: // alt screen cursor - this.saveCursor(params); + this.saveCursor(); break; case 1049: // alt screen buffer cursor - this.saveCursor(params); + this.saveCursor(); // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer @@ -1477,17 +1421,15 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: number[], collect?: string): void { - if (params.length > 1) { - for (let i = 0; i < params.length; i++) { - this.resetMode([params[i]]); - } - - return; + public resetMode(params: IParams, collect?: string): void { + for (let i = 0; i < params.length; i++) { + this._resetMode(params.params[i], collect); } + } + private _resetMode(param: number, collect?: string): void { if (!collect) { - switch (params[0]) { + switch (param) { case 4: this._terminal.insertMode = false; break; @@ -1496,7 +1438,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } else if (collect === '?') { - switch (params[0]) { + switch (param) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = false; break; @@ -1553,7 +1495,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.cursorHidden = true; break; case 1048: // alt screen cursor - this.restoreCursor(params); + this.restoreCursor(); break; case 1049: // alt screen buffer cursor // FALL-THROUGH @@ -1561,8 +1503,8 @@ export class InputHandler extends Disposable implements IInputHandler { case 1047: // normal screen buffer - clearing it first // Ensure the selection manager has the correct buffer this._terminal.buffers.activateNormalBuffer(); - if (params[0] === 1049) { - this.restoreCursor(params); + if (param === 1049) { + this.restoreCursor(); } this._terminal.refresh(0, this._terminal.rows - 1); if (this._terminal.viewport) { @@ -1577,6 +1519,78 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** + * Helper to extract and apply color params/subparams. + * Returns advance for params index. + */ + private _extractColor(params: IParams, pos: number, attr: IAttributeData): number { + // normalize params + // meaning: [target, CM, ign, val, val, val] + // RGB : [ 38/48, 2, ign, r, g, b] + // P256 : [ 38/48, 5, ign, v, ign, ign] + const accu = [0, 0, -1, 0, 0, 0]; + + // alignment placeholder for non color space sequences + let cSpace = 0; + + // return advance we took in params + let advance = 0; + + do { + accu[advance + cSpace] = params.params[pos + advance]; + if (params.hasSubParams(pos + advance)) { + const subparams = params.getSubParams(pos + advance); + let i = 0; + do { + if (accu[1] === 5) { + cSpace = 1; + } + accu[advance + i + 1 + cSpace] = subparams[i]; + } while (++i < subparams.length && i + advance + 1 + cSpace < accu.length); + break; + } + // exit early if can decide color mode with semicolons + if ((accu[1] === 5 && advance + cSpace >= 2) + || (accu[1] === 2 && advance + cSpace >= 5)) { + break; + } + // offset colorSpace slot for semicolon mode + if (accu[1]) { + cSpace = 1; + } + } while (++advance + pos < params.length && advance + cSpace < accu.length); + + // set default values to 0 + for (let i = 2; i < accu.length; ++i) { + if (accu[i] === -1) { + accu[i] = 0; + } + } + + // apply colors + if (accu[0] === 38) { + if (accu[1] === 2) { + attr.fg |= Attributes.CM_RGB; + attr.fg &= ~Attributes.RGB_MASK; + attr.fg |= AttributeData.fromColorRGB([accu[3], accu[4], accu[5]]); + } else if (accu[1] === 5) { + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P256 | (accu[3] & 0xff); + } + } else if (accu[0] === 48) { + if (accu[1] === 2) { + attr.bg |= Attributes.CM_RGB; + attr.bg &= ~Attributes.RGB_MASK; + attr.bg |= AttributeData.fromColorRGB([accu[3], accu[4], accu[5]]); + } else if (accu[1] === 5) { + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P256 | (accu[3] & 0xff); + } + } + + return advance; + } + /** * CSI Pm m Character Attributes (SGR). * Ps = 0 -> Normal (default). @@ -1642,9 +1656,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 4 8 ; 5 ; Ps -> Set background color to the second * Ps. */ - public charAttributes(params: number[]): void { + public charAttributes(params: IParams): void { // Optimize a single SGR0. - if (params.length === 1 && params[0] === 0) { + if (params.length === 1 && params.params[0] === 0) { this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg; this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg; return; @@ -1655,7 +1669,7 @@ export class InputHandler extends Disposable implements IInputHandler { const attr = this._terminal.curAttrData; for (let i = 0; i < l; i++) { - p = params[i]; + p = params.params[i]; if (p >= 30 && p <= 37) { // fg color 8 attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); @@ -1725,34 +1739,9 @@ export class InputHandler extends Disposable implements IInputHandler { // reset bg attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); - } else if (p === 38) { + } else if (p === 38 || p === 48) { // fg color 256 and RGB - if (params[i + 1] === 2) { - i += 2; - attr.fg |= Attributes.CM_RGB; - attr.fg &= ~Attributes.RGB_MASK; - attr.fg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]); - i += 2; - } else if (params[i + 1] === 5) { - i += 2; - p = params[i] & 0xff; - attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); - attr.fg |= Attributes.CM_P256 | p; - } - } else if (p === 48) { - // bg color 256 and RGB - if (params[i + 1] === 2) { - i += 2; - attr.bg |= Attributes.CM_RGB; - attr.bg &= ~Attributes.RGB_MASK; - attr.bg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]); - i += 2; - } else if (params[i + 1] === 5) { - i += 2; - p = params[i] & 0xff; - attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); - attr.bg |= Attributes.CM_P256 | p; - } + i += this._extractColor(params, i, attr); } else if (p === 100) { // reset fg/bg attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); @@ -1788,9 +1777,9 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: number[], collect?: string): void { + public deviceStatus(params: IParams, collect?: string): void { if (!collect) { - switch (params[0]) { + switch (params.params[0]) { case 5: // status report this._coreService.triggerDataEvent(`${C0.ESC}[0n`); @@ -1805,7 +1794,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (collect === '?') { // modern xterm doesnt seem to // respond to any of these except ?6, 6, and 5 - switch (params[0]) { + switch (params.params[0]) { case 6: // cursor position const y = this._terminal.buffer.y + 1; @@ -1836,7 +1825,7 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: number[], collect?: string): void { + public softReset(params: IParams, collect?: string): void { if (collect === '!') { this._terminal.cursorHidden = false; this._terminal.insertMode = false; @@ -1867,9 +1856,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: number[], collect?: string): void { + public setCursorStyle(params?: IParams, collect?: string): void { if (collect === ' ') { - const param = params[0] < 1 ? 1 : params[0]; + const param = params.params[0] || 1; switch (param) { case 1: case 2: @@ -1894,13 +1883,14 @@ export class InputHandler extends Disposable implements IInputHandler { * Set Scrolling Region [top;bottom] (default = full size of win- * dow) (DECSTBM). * CSI ? Pm r + * currently skipped */ - public setScrollRegion(params: number[], collect?: string): void { + public setScrollRegion(params: IParams, collect?: string): void { if (collect) { return; } - this._terminal.buffer.scrollTop = (params[0] || 1) - 1; - this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1; + this._terminal.buffer.scrollTop = (params.params[0] || 1) - 1; + this._terminal.buffer.scrollBottom = (params.length > 1 && params.params[1] && params.params[1] <= this._terminal.rows ? params.params[1] : this._terminal.rows) - 1; this._terminal.buffer.x = 0; this._terminal.buffer.y = 0; } @@ -1911,7 +1901,7 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC 7 * Save cursor (ANSI.SYS). */ - public saveCursor(params: number[]): void { + public saveCursor(params?: IParams): void { this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedY = this._terminal.buffer.ybase + this._terminal.buffer.y; this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; @@ -1924,7 +1914,7 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC 8 * Restore cursor (ANSI.SYS). */ - public restoreCursor(params: number[]): void { + public restoreCursor(params?: IParams): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = Math.max(this._terminal.buffer.savedY - this._terminal.buffer.ybase, 0); this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg; @@ -2088,7 +2078,7 @@ export class InputHandler extends Disposable implements IInputHandler { const buffer = this._terminal.buffer; - this.cursorPosition([1, 1]); + this._setCursor(0, 0); for (let yOffset = 0; yOffset < this._terminal.rows; ++yOffset) { const row = buffer.y + buffer.ybase + yOffset; buffer.lines.get(row).fill(cell); @@ -2096,6 +2086,6 @@ export class InputHandler extends Disposable implements IInputHandler { } this._terminal.updateRange(0); this._terminal.updateRange(this._terminal.rows); - this.cursorPosition([1, 1]); + this._setCursor(0, 0); } } diff --git a/src/Terminal.ts b/src/Terminal.ts index f8360117..ccec42e8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -34,7 +34,7 @@ import { SelectionService } from './browser/services/SelectionService'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; import * as Strings from './browser/LocalizableStrings'; -import { SoundManager } from './SoundManager'; +import { SoundService } from 'browser/services/SoundService'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; @@ -49,13 +49,14 @@ import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; import { IOptionsService, IBufferService, ICoreService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; +import { IParams } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; // Let it work inside Node.js for automated testing purposes. @@ -116,6 +117,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _mouseService: IMouseService; private _renderService: IRenderService; private _selectionService: ISelectionService; + private _soundService: ISoundService; // modes public applicationKeypad: boolean; @@ -174,7 +176,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _userScrolling: boolean; private _inputHandler: InputHandler; - public soundManager: SoundManager; public linkifier: ILinkifier; public viewport: IViewport; private _compositionHelper: ICompositionHelper; @@ -304,7 +305,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._selectionService = this._selectionService || null; this.linkifier = this.linkifier || new Linkifier(this); this._mouseZoneManager = this._mouseZoneManager || null; - this.soundManager = this.soundManager || new SoundManager(this); if (this.options.windowsMode) { this._windowsMode = applyWindowsMode(this); @@ -619,6 +619,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._renderService.onRender(e => this._onRender.fire(e)); this.onResize(e => this._renderService.resize(e.cols, e.rows)); + this._soundService = new SoundService(this.optionsService); this._mouseService = new MouseService(this._renderService, this._charSizeService); this._mouseZoneManager = new MouseZoneManager(this, this._mouseService); @@ -1413,7 +1414,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { return this._inputHandler.addCsiHandler(flag, callback); } /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ @@ -1676,7 +1677,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ public bell(): void { if (this._soundBell()) { - this.soundManager.playBellSound(); + this._soundService.playBellSound(); } if (this._visualBell()) { diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index b203d1a1..62ae609e 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -15,6 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorManager, IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; +import { IParams } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { @@ -71,7 +72,7 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { throw new Error('Method not implemented.'); } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { diff --git a/src/Types.d.ts b/src/Types.d.ts index 0a45f792..1ebc800e 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -9,6 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IParams } from 'common/parser/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -109,42 +110,42 @@ export interface IInputHandler { /** C0 SO */ shiftOut(): void; /** C0 SI */ shiftIn(): void; - /** CSI @ */ insertChars(params?: number[]): void; - /** CSI A */ cursorUp(params?: number[]): void; - /** CSI B */ cursorDown(params?: number[]): void; - /** CSI C */ cursorForward(params?: number[]): void; - /** CSI D */ cursorBackward(params?: number[]): void; - /** CSI E */ cursorNextLine(params?: number[]): void; - /** CSI F */ cursorPrecedingLine(params?: number[]): void; - /** CSI G */ cursorCharAbsolute(params?: number[]): void; - /** CSI H */ cursorPosition(params?: number[]): void; - /** CSI I */ cursorForwardTab(params?: number[]): void; - /** CSI J */ eraseInDisplay(params?: number[]): void; - /** CSI K */ eraseInLine(params?: number[]): void; - /** CSI L */ insertLines(params?: number[]): void; - /** CSI M */ deleteLines(params?: number[]): void; - /** CSI P */ deleteChars(params?: number[]): void; - /** CSI S */ scrollUp(params?: number[]): void; - /** CSI T */ scrollDown(params?: number[], collect?: string): void; - /** CSI X */ eraseChars(params?: number[]): void; - /** CSI Z */ cursorBackwardTab(params?: number[]): void; - /** CSI ` */ charPosAbsolute(params?: number[]): void; - /** CSI a */ hPositionRelative(params?: number[]): void; - /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; - /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void; - /** CSI d */ linePosAbsolute(params?: number[]): void; - /** CSI e */ vPositionRelative(params?: number[]): void; - /** CSI f */ hVPosition(params?: number[]): void; - /** CSI g */ tabClear(params?: number[]): void; - /** CSI h */ setMode(params?: number[], collect?: string): void; - /** CSI l */ resetMode(params?: number[], collect?: string): void; - /** CSI m */ charAttributes(params?: number[]): void; - /** CSI n */ deviceStatus(params?: number[], collect?: string): void; - /** CSI p */ softReset(params?: number[], collect?: string): void; - /** CSI q */ setCursorStyle(params?: number[], collect?: string): void; - /** CSI r */ setScrollRegion(params?: number[], collect?: string): void; - /** CSI s */ saveCursor(params?: number[]): void; - /** CSI u */ restoreCursor(params?: number[]): void; + /** CSI @ */ insertChars(params: IParams): void; + /** CSI A */ cursorUp(params: IParams): void; + /** CSI B */ cursorDown(params: IParams): void; + /** CSI C */ cursorForward(params: IParams): void; + /** CSI D */ cursorBackward(params: IParams): void; + /** CSI E */ cursorNextLine(params: IParams): void; + /** CSI F */ cursorPrecedingLine(params: IParams): void; + /** CSI G */ cursorCharAbsolute(params: IParams): void; + /** CSI H */ cursorPosition(params: IParams): void; + /** CSI I */ cursorForwardTab(params: IParams): void; + /** CSI J */ eraseInDisplay(params: IParams): void; + /** CSI K */ eraseInLine(params: IParams): void; + /** CSI L */ insertLines(params: IParams): void; + /** CSI M */ deleteLines(params: IParams): void; + /** CSI P */ deleteChars(params: IParams): void; + /** CSI S */ scrollUp(params: IParams): void; + /** CSI T */ scrollDown(params: IParams, collect?: string): void; + /** CSI X */ eraseChars(params: IParams): void; + /** CSI Z */ cursorBackwardTab(params: IParams): void; + /** CSI ` */ charPosAbsolute(params: IParams): void; + /** CSI a */ hPositionRelative(params: IParams): void; + /** CSI b */ repeatPrecedingCharacter(params: IParams): void; + /** CSI c */ sendDeviceAttributes(params: IParams, collect?: string): void; + /** CSI d */ linePosAbsolute(params: IParams): void; + /** CSI e */ vPositionRelative(params: IParams): void; + /** CSI f */ hVPosition(params: IParams): void; + /** CSI g */ tabClear(params: IParams): void; + /** CSI h */ setMode(params: IParams, collect?: string): void; + /** CSI l */ resetMode(params: IParams, collect?: string): void; + /** CSI m */ charAttributes(params: IParams): void; + /** CSI n */ deviceStatus(params: IParams, collect?: string): void; + /** CSI p */ softReset(params: IParams, collect?: string): void; + /** CSI q */ setCursorStyle(params: IParams, collect?: string): void; + /** CSI r */ setScrollRegion(params: IParams, collect?: string): void; + /** CSI s */ saveCursor(params: IParams): void; + /** CSI u */ restoreCursor(params: IParams): void; /** OSC 0 OSC 2 */ setTitle(data: string): void; /** ESC E */ nextLine(): void; @@ -240,7 +241,7 @@ export interface IPublicTerminal extends IDisposable { writeln(data: string): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; @@ -347,10 +348,6 @@ export interface IBrowser { isWindows: boolean; } -export interface ISoundManager { - playBellSound(): void; -} - export interface IMouseZoneManager extends IDisposable { add(zone: IMouseZone): void; clearAll(start?: number, end?: number): void; diff --git a/src/browser/services/Services.d.ts b/src/browser/services/Services.d.ts index b22a0b37..603d4109 100644 --- a/src/browser/services/Services.d.ts +++ b/src/browser/services/Services.d.ts @@ -72,3 +72,7 @@ export interface ISelectionService { refresh(isLinuxMouseSelection?: boolean): void; onMouseDown(event: MouseEvent): void; } + +export interface ISoundService { + playBellSound(): void; +} diff --git a/src/SoundManager.ts b/src/browser/services/SoundService.ts similarity index 74% rename from src/SoundManager.ts rename to src/browser/services/SoundService.ts index 6bff444f..31380031 100644 --- a/src/SoundManager.ts +++ b/src/browser/services/SoundService.ts @@ -3,35 +3,36 @@ * @license MIT */ -import { ITerminal, ISoundManager } from './Types'; +import { IOptionsService } from 'common/services/Services'; +import { ISoundService } from 'browser/services/Services'; -export class SoundManager implements ISoundManager { +export class SoundService implements ISoundService { private static _audioContext: AudioContext; static get audioContext(): AudioContext | null { - if (!SoundManager._audioContext) { + if (!SoundService._audioContext) { const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; if (!audioContextCtor) { console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); return null; } - SoundManager._audioContext = new audioContextCtor(); + SoundService._audioContext = new audioContextCtor(); } - return SoundManager._audioContext; + return SoundService._audioContext; } constructor( - private _terminal: ITerminal + private _optionsService: IOptionsService ) { } public playBellSound(): void { - const ctx = SoundManager.audioContext; + const ctx = SoundService.audioContext; if (!ctx) { return; } const bellAudioSource = ctx.createBufferSource(); - ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { + ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.options.bellSound)), (buffer) => { bellAudioSource.buffer = buffer; bellAudioSource.connect(ctx.destination); bellAudioSource.start(0); diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index f445efc4..90ec0ee0 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,11 +3,13 @@ * @license MIT */ -import { IDcsHandler, IParsingState } from 'common/parser/Types'; +import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; import { ParserState } from 'common/parser/Constants'; +import { Params } from 'common/parser/Params'; + function r(a: number, b: number): string[] { let c = b - a; @@ -26,11 +28,14 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public set osc(value: string) { this._osc = value; } - public get params(): number[] { - return this._params; + public get params(): ParamsArray { + return this._params.toArray(); } - public set params(value: number[]) { - this._params = value; + public set params(value: ParamsArray) { + this._params = Params.fromArray(value); + } + public get realParams(): IParams { + return this._params; } public get collect(): string { return this._collect; @@ -65,14 +70,14 @@ const testTerminal: any = { actionExecute: function (flag: string): void { this.calls.push(['exe', flag]); }, - actionCSI: function (collect: string, params: number[], flag: string): void { - this.calls.push(['csi', collect, params, flag]); + actionCSI: function (collect: string, params: IParams, flag: string): void { + this.calls.push(['csi', collect, params.toArray(), flag]); }, actionESC: function (collect: string, flag: string): void { this.calls.push(['esc', collect, flag]); }, - actionDCSHook: function (collect: string, params: number[], flag: string): void { - this.calls.push(['dcs hook', collect, params, flag]); + actionDCSHook: function (collect: string, params: IParams, flag: string): void { + this.calls.push(['dcs hook', collect, params.toArray(), flag]); }, actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { let s = ''; @@ -88,7 +93,7 @@ const testTerminal: any = { // dcs handler to map dcs actions into the test object `testTerminal` class DcsTest implements IDcsHandler { - hook(collect: string, params: number[], flag: number): void { + hook(collect: string, params: IParams, flag: number): void { testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); } put(data: Uint32Array, start: number, end: number): void { @@ -120,7 +125,7 @@ let state: any; // parser with Uint8Array based transition table const testParser = new TestEscapeSequenceParser(); testParser.setPrintHandler(testTerminal.print.bind(testTerminal)); -testParser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { +testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); }); testParser.setEscHandlerFallback((collect: string, flag: number) => { @@ -562,16 +567,16 @@ describe('EscapeSequenceParser', function (): void { testTerminal.clear(); } }); - it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + it('trans CSI_ENTRY --> CSI_PARAM for ":" (0x3a)', function (): void { parser.reset(); parser.currentState = ParserState.CSI_ENTRY; parse(parser, '\x3a'); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); parser.reset(); }); it('trans CSI_PARAM --> CSI_IGNORE', function (): void { parser.reset(); - const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + const chars = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parse(parser, '\x3b' + chars[i]); @@ -580,6 +585,18 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); } }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + const chars = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + chai.expect(parser.params).eql([0]); + parser.currentState = ParserState.CSI_PARAM; + parse(parser, '\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { parser.reset(); const chars = r(0x30, 0x40); @@ -785,16 +802,16 @@ describe('EscapeSequenceParser', function (): void { chai.expect(parser.params).eql([0, 0]); parser.reset(); }); - it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + it('trans DCS_ENTRY --> DCS_PARAM for ":" (0x3a)', function (): void { parser.reset(); parser.currentState = ParserState.DCS_ENTRY; parse(parser, '\x3a'); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); parser.reset(); }); it('trans DCS_PARAM --> DCS_IGNORE', function (): void { parser.reset(); - const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + const chars = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_PARAM; parse(parser, '\x3b' + chars[i]); @@ -1028,6 +1045,23 @@ describe('EscapeSequenceParser', function (): void { ['print', 'defg'] ], null); }); + it('colon notation in CSI params', () => { + test('\x1b[<31;5::123:;8mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5, [-1, 123, -1], 8], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('colon notation in DCS params', function (): void { + test('abc\x901;2::55;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, [-1, 55], 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); }); describe('coverage tests', function (): void { @@ -1078,10 +1112,10 @@ describe('EscapeSequenceParser', function (): void { let parser2: TestEscapeSequenceParser; let print = ''; const esc: string[] = []; - const csi: [string, number[], string][] = []; + const csi: [string, ParamsArray, string][] = []; const exe: string[] = []; const osc: [number, string][] = []; - const dcs: ([string] | [string, string] | [string, string, number[], number])[] = []; + const dcs: ([string] | [string, string] | [string, string, ParamsArray, number])[] = []; function clearAccu(): void { print = ''; esc.length = 0; @@ -1128,8 +1162,8 @@ describe('EscapeSequenceParser', function (): void { chai.expect(esc).eql([]); }); it('CSI handler', function (): void { - parser2.setCsiHandler('m', function (params: number[], collect: string): void { - csi.push(['m', params, collect]); + parser2.setCsiHandler('m', function (params: IParams, collect: string): void { + csi.push(['m', params.toArray(), collect]); }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1141,38 +1175,38 @@ describe('EscapeSequenceParser', function (): void { }); describe('CSI custom handlers', () => { it('Prevent fallback', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + const csiCustom: [string, ParamsArray, string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); it('Allow fallback', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); + const csiCustom: [string, ParamsArray, string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); it('Multiple custom handlers fallback once', () => { - const csiCustom: [string, number[], string][] = []; - const csiCustom2: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); + const csiCustom: [string, ParamsArray, string][] = []; + const csiCustom2: [string, ParamsArray, string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); }); it('Multiple custom handlers no fallback', () => { - const csiCustom: [string, number[], string][] = []; - const csiCustom2: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); + const csiCustom: [string, ParamsArray, string][] = []; + const csiCustom2: [string, ParamsArray, string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([], 'Should not fallback once'); @@ -1187,18 +1221,18 @@ describe('EscapeSequenceParser', function (): void { chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + const csiCustom: [string, ParamsArray, string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); customHandler.dispose(); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); it('Should not corrupt the parser when dispose is called twice', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + const csiCustom: [string, ParamsArray, string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1302,8 +1336,8 @@ describe('EscapeSequenceParser', function (): void { }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { - hook: function (collect: string, params: number[], flag: number): void { - dcs.push(['hook', collect, params, flag]); + hook: function (collect: string, params: IParams, flag: number): void { + dcs.push(['hook', collect, params.toArray(), flag]); }, put: function (data: Uint32Array, start: number, end: number): void { let s = ''; @@ -1343,7 +1377,7 @@ describe('EscapeSequenceParser', function (): void { currentState: ParserState.CSI_PARAM, osc: '', collect: '', - params: [1, 2, 0], // extra zero here + params: Params.fromArray([1, 2, 0]), // extra zero here abort: false }); parser2.clearErrorHandler(); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 9725b8bc..1d2b7e1e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,18 +3,19 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; +import { Params } from 'common/parser/Params'; interface IHandlerCollection { [key: string]: T[]; } -type CsiHandler = (params: number[], collect: string) => boolean | void; +type CsiHandler = (params: IParams, collect: string) => boolean | void; type OscHandler = (data: string) => boolean | void; /** @@ -143,17 +144,14 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { // csi entries table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); - table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); - table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM); - table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); - table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND); - table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE); table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND); - table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE); table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE); @@ -173,9 +171,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); - table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE); - table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); - table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM); table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); @@ -183,9 +179,8 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); - table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); - table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); - table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE); table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); @@ -212,7 +207,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { * Dummy DCS handler as default fallback. */ class DcsDummy implements IDcsHandler { - hook(collect: string, params: number[], flag: number): void { } + hook(collect: string, params: IParams, flag: number): void { } put(data: Uint32Array, start: number, end: number): void { } unhook(): void { } } @@ -221,12 +216,22 @@ class DcsDummy implements IDcsHandler { * EscapeSequenceParser. * This class implements the ANSI/DEC compatible parser described by * Paul Williams (https://vt100.net/emu/dec_ansi_parser). + * * To implement custom ANSI compliant escape sequences it is not needed to * alter this parser, instead consider registering a custom handler. * For non ANSI compliant sequences change the transition table with * the optional `transitions` contructor argument and * reimplement the `parse` method. - * NOTE: The parameter element notation is currently not supported. + * + * This parser is currently hardcoded to operate in ZDM (Zero Default Mode) + * as suggested by the original parser, thus empty parameters are set to 0. + * This this is not in line with the latest ECMA specification + * (ZDM was part of the early specs and got completely removed later on). + * + * Other than the original parser from vt100.net this parser supports + * sub parameters in digital parameters separated by colons. Empty sub parameters + * are set to -1. + * * TODO: implement error recovery hook via error handler return values */ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser { @@ -236,7 +241,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // buffers over several parse calls protected _osc: string; - protected _params: number[]; + protected _params: Params; protected _collect: string; // handler lookup containers @@ -252,7 +257,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // fallback handlers protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlerFb: (code: number) => void; - protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; + protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; protected _oscHandlerFb: (identifier: number, data: string) => void; protected _dcsHandlerFb: IDcsHandler; @@ -264,14 +269,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.initialState = ParserState.GROUND; this.currentState = this.initialState; this._osc = ''; - this._params = [0]; + this._params = new Params(); // defaults to 32 storable params/subparams + this._params.addParam(0); // ZDM this._collect = ''; this.precedingCodepoint = 0; // set default fallback handlers and handler lookup containers this._printHandlerFb = (data, start, end): void => { }; this._executeHandlerFb = (code: number): void => { }; - this._csiHandlerFb = (collect: string, params: number[], flag: number): void => { }; + this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { }; this._escHandlerFb = (collect: string, flag: number): void => { }; this._oscHandlerFb = (identifier: number, data: string): void => { }; this._dcsHandlerFb = new DcsDummy(); @@ -329,13 +335,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { + setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void { this._csiHandlers[flag.charCodeAt(0)] = [callback]; } clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; } - setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void { + setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void { this._csiHandlerFb = callback; } @@ -394,7 +400,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP reset(): void { this.currentState = this.initialState; this._osc = ''; - this._params = [0]; + this._params.reset(); + this._params.addParam(0); // ZDM this._collect = ''; this._activeDcsHandler = null; this.precedingCodepoint = 0; @@ -402,10 +409,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP /** * Parse UTF32 codepoints in `data` up to `length`. + * * Note: For several actions with high data load the parsing is optimized * by using local read ahead loops with hardcoded conditions to * avoid costly table lookups. Make sure that any change of table values - * will be reflected in the loop conditions as well. Affected states/actions: + * will be reflected in the loop conditions as well and vice versa. + * Affected states/actions: * - GROUND:PRINT * - CSI_PARAM:PARAM * - DCS_PARAM:PARAM @@ -418,7 +427,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let currentState = this.currentState; let osc = this._osc; let collect = this._collect; - let params = this._params; + const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; let dcsHandler: IDcsHandler | null = this._activeDcsHandler; let callback: Function | null = null; @@ -494,11 +503,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.precedingCodepoint = 0; break; case ParserAction.PARAM: - // inner loop: digits (0x30 - 0x39) and ; (0x3b) + // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a) + let isSub = false; do { - if (code === 0x3b) params.push(0); - else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; - } while (++i < length && (code = data[i]) > 0x2f && (code < 0x3a || code === 0x3b)); + switch (code) { + case 0x3b: + params.addParam(0); // ZDM + isSub = false; + break; + case 0x3a: + params.addSubParam(-1); + isSub = true; + break; + default: // 0x30 - 0x39 + if (isSub) params.addSubParamDigit(code - 48); + else params.addParamDigit(code - 48); + } + } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c); i--; break; case ParserAction.COLLECT: @@ -512,7 +533,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.CLEAR: osc = ''; - params = [0]; + params.reset(); + params.addParam(0); // ZDM collect = ''; break; case ParserAction.DCS_HOOK: @@ -540,7 +562,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } if (code === 0x1b) transition |= ParserState.ESCAPE; osc = ''; - params = [0]; + params.reset(); + params.addParam(0); // ZDM collect = ''; this.precedingCodepoint = 0; break; @@ -586,7 +609,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } if (code === 0x1b) transition |= ParserState.ESCAPE; osc = ''; - params = [0]; + params.reset(); + params.addParam(0); // ZDM collect = ''; this.precedingCodepoint = 0; break; diff --git a/src/common/parser/Params.test.ts b/src/common/parser/Params.test.ts new file mode 100644 index 00000000..01287b8f --- /dev/null +++ b/src/common/parser/Params.test.ts @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { Params } from 'common/parser/Params'; +import { ParamsArray } from 'common/parser/Types'; + +class TestParams extends Params { + public get subParams(): Int32Array { + return this._subParams; + } + public get subParamsLength(): number { + return this._subParamsLength; + } +} + +/** `Params` parser shim */ +function parse(params: Params, s: string): void { + params.reset(); + params.addParam(0); + let isSub = false; + for (let i = 0; i < s.length; ++i) { + let code = s.charCodeAt(i); + do { + switch (code) { + case 0x3b: + params.addParam(0); + isSub = false; + break; + case 0x3a: + params.addSubParam(-1); + isSub = true; + break; + default: // 0x30 - 0x39 + if (isSub) params.addSubParamDigit(code - 48); + else params.addParamDigit(code - 48); + } + } while (++i < s.length && (code = s.charCodeAt(i)) > 0x2f && code < 0x3c); + } +} + + +describe('Params', () => { + it('should respect ctor args', () => { + const params = new TestParams(12, 23); + assert.equal(params.params.length, 12); + assert.equal(params.subParams.length, 23); + assert.deepEqual(params.toArray(), []); + }); + it('addParam', () => { + const params = new TestParams(); + params.addParam(1); + assert.equal(params.length, 1); + assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1]); + assert.deepEqual(params.toArray(), [1]); + params.addParam(23); + assert.equal(params.length, 2); + assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1, 23]); + assert.deepEqual(params.toArray(), [1, 23]); + assert.equal(params.subParamsLength, 0); + }); + it('addSubParam', () => { + const params = new TestParams(); + params.addParam(1); + params.addSubParam(2); + params.addSubParam(3); + assert.equal(params.length, 1); + assert.equal(params.subParamsLength, 2); + assert.deepEqual(params.toArray(), [1, [2, 3]]); + params.addParam(12345); + params.addSubParam(-1); + assert.equal(params.length, 2); + assert.equal(params.subParamsLength, 3); + assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]); + }); + it('should not add sub params without previous param', () => { + const params = new TestParams(); + params.addSubParam(2); + params.addSubParam(3); + assert.equal(params.length, 0); + assert.equal(params.subParamsLength, 0); + assert.deepEqual(params.toArray(), []); + params.addParam(1); + params.addSubParam(2); + params.addSubParam(3); + assert.equal(params.length, 1); + assert.equal(params.subParamsLength, 2); + assert.deepEqual(params.toArray(), [1, [2, 3]]); + }); + it('reset', () => { + const params = new TestParams(); + params.addParam(1); + params.addSubParam(2); + params.addSubParam(3); + params.addParam(12345); + params.addSubParam(-1); + params.reset(); + assert.equal(params.length, 0); + assert.equal(params.subParamsLength, 0); + assert.deepEqual(params.toArray(), []); + params.addParam(1); + params.addSubParam(2); + params.addSubParam(3); + params.addParam(12345); + params.addSubParam(-1); + assert.equal(params.length, 2); + assert.equal(params.subParamsLength, 3); + assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]); + }); + it('Params.fromArray --> toArray', () => { + let data: ParamsArray = []; + assert.deepEqual(Params.fromArray(data).toArray(), data); + data = [1, [2, 3], 12345, [-1]]; + assert.deepEqual(Params.fromArray(data).toArray(), data); + data = [38, 2, 50, 100, 150]; + assert.deepEqual(Params.fromArray(data).toArray(), data); + data = [38, 2, 50, 100, [150]]; + assert.deepEqual(Params.fromArray(data).toArray(), data); + data = [38, [2, 50, 100, 150]]; + assert.deepEqual(Params.fromArray(data).toArray(), data); + // strip empty sub params + data = [38, [2, 50, 100, 150], 5, [], 6]; + assert.deepEqual(Params.fromArray(data).toArray(), [38, [2, 50, 100, 150], 5, 6]); + }); + it('clone', () => { + const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6, 1, [2, 3], 12345, [-1]]); + assert.deepEqual(params.clone(), params); + }); + it('hasSubParams / getSubParams', () => { + const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6]); + assert.equal(params.hasSubParams(0), true); + assert.deepEqual(params.getSubParams(0), new Int32Array([2, 50, 100, 150])); + assert.equal(params.hasSubParams(1), false); + assert.deepEqual(params.getSubParams(1), null); + assert.equal(params.hasSubParams(2), false); + assert.deepEqual(params.getSubParams(2), null); + }); + it('getSubParamsAll', () => { + const params = Params.fromArray([1, [2, 3], 7, 12345, [-1]]); + assert.deepEqual(params.getSubParamsAll(), {0: new Int32Array([2, 3]), 2: new Int32Array([-1])}); + }); + describe('parse tests', () => { + it('param defaults to 0 (ZDM - zero default mode)', () => { + const params = new Params(); + parse(params, ''); + assert.deepEqual(params.toArray(), [0]); + }); + it('sub param defaults to -1', () => { + const params = new Params(); + parse(params, ':'); + assert.deepEqual(params.toArray(), [0, [-1]]); + }); + it('should correctly reset on new sequence', () => { + const params = new Params(); + parse(params, '1;2;3'); + assert.deepEqual(params.toArray(), [1, 2, 3]); + parse(params, '4'); + assert.deepEqual(params.toArray(), [4]); + parse(params, '4::123:5;6;7'); + assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]); + parse(params, ''); + assert.deepEqual(params.toArray(), [0]); + }); + it('should handle length restrictions correctly', () => { + // restrict to 3 params and 3 sub params + const params = new Params(3, 3); + parse(params, '1;2;3'); + assert.deepEqual(params.toArray(), [1, 2, 3]); + parse(params, '4'); + assert.deepEqual(params.toArray(), [4]); + parse(params, '4::123:5;6;7'); + assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]); + parse(params, ''); + assert.deepEqual(params.toArray(), [0]); + // overlong params + parse(params, '1;2;3;4;5;6;7'); + assert.deepEqual(params.toArray(), [1, 2, 3]); + // overlong sub params + parse(params, '4;38:2::50:100:150;48:5:22'); + assert.deepEqual(params.toArray(), [4, 38, [2, -1, 50], 48]); + }); + it('typical sequences', () => { + const params = new Params(); + // SGR with semicolon syntax + parse(params, '0;4;38;2;50;100;150;48;5;22'); + assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, 100, 150, 48, 5, 22]); + // SGR mixed style (partly wrong) + parse(params, '0;4;38;2;50:100:150;48;5:22'); + assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, [100, 150], 48, 5, [22]]); + // SGR colon style + parse(params, '0;4;38:2::50:100:150;48:5:22'); + assert.deepEqual(params.toArray(), [0, 4, 38, [2, -1, 50, 100, 150], 48, [5, 22]]); + }); + }); + describe('should not overflow to negative', () => { + it('reject params lesser -1', () => { + const params = new Params(); + params.addParam(-1); + assert.throws(() => params.addParam(-2), 'values lesser than -1 are not allowed'); + }); + it('reject subparams lesser -1', () => { + const params = new Params(); + params.addParam(-1); + params.addSubParam(-1); + assert.throws(() => params.addSubParam(-2), 'values lesser than -1 are not allowed'); + assert.deepEqual(params.toArray(), [-1, [-1]]); + }); + it('clamp parsed params', () => { + const params = new Params(); + parse(params, '2147483648'); + assert.deepEqual(params.toArray(), [0x7FFFFFFF]); + }); + it('clamp parsed subparams', () => { + const params = new Params(); + parse(params, ':2147483648'); + assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]); + }); + }); +}); diff --git a/src/common/parser/Params.ts b/src/common/parser/Params.ts new file mode 100644 index 00000000..7ef8341a --- /dev/null +++ b/src/common/parser/Params.ts @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IParams, ParamsArray } from 'common/parser/Types'; + +// max value supported for a single param/subparam (clamped to positive int32 range) +const MAX_VALUE = 0x7FFFFFFF; +// max allowed subparams for a single sequence (hardcoded limitation) +const MAX_SUBPARAMS = 256; + +/** + * Params storage class. + * This type is used by the parser to accumulate sequence parameters and sub parameters + * and transmit them to the input handler actions. + * + * NOTES: + * - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy + * - never read beyond `params.length - 1` (likely to contain arbitrary data) + * - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params + * - hardcoded limitations: + * - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that) + * - max. 256 sub params possible + * - negative values are not allowed beside -1 (placeholder for default value) + * + * About ZDM (Zero Default Mode): + * ZDM is not orchestrated by this class. If the parser is in ZDM, + * it should add 0 for empty params, otherwise -1. This does not apply + * to subparams, empty subparams should always be added with -1. + */ +export class Params implements IParams { + // params store and length + public params: Int32Array; + public length: number; + + // sub params store and length + protected _subParams: Int32Array; + protected _subParamsLength: number; + + // sub params offsets from param: param idx --> [start, end] offset + private _subParamsIdx: Uint16Array; + private _rejectDigits: boolean; + private _rejectSubDigits: boolean; + + /** + * Create a `Params` type from JS array representation. + */ + public static fromArray(values: ParamsArray): Params { + const params = new Params(); + if (!values.length) { + return params; + } + // skip leading sub params + for (let i = (values[0] instanceof Array) ? 1 : 0; i < values.length; ++i) { + const value = values[i]; + if (value instanceof Array) { + for (let k = 0; k < value.length; ++k) { + params.addSubParam(value[k]); + } + } else { + params.addParam(value); + } + } + return params; + } + + /** + * @param maxLength max length of storable parameters + * @param maxSubParamsLength max length of storable sub parameters + */ + constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) { + if (maxSubParamsLength > MAX_SUBPARAMS) { + throw new Error('maxSubParamsLength must not be greater than 256'); + } + this.params = new Int32Array(maxLength); + this.length = 0; + this._subParams = new Int32Array(maxSubParamsLength); + this._subParamsLength = 0; + this._subParamsIdx = new Uint16Array(maxLength); + this._rejectDigits = false; + this._rejectSubDigits = false; + } + + /** + * Clone object. + */ + public clone(): Params { + const newParams = new Params(this.maxLength, this.maxSubParamsLength); + newParams.params.set(this.params); + newParams.length = this.length; + newParams._subParams.set(this._subParams); + newParams._subParamsLength = this._subParamsLength; + newParams._subParamsIdx.set(this._subParamsIdx); + return newParams; + } + + /** + * Get a JS array representation of the current parameters and sub parameters. + * The array is structured as follows: + * sequence: "1;2:3:4;5::6" + * array : [1, 2, [3, 4], 5, [-1, 6]] + */ + public toArray(): ParamsArray { + const res: ParamsArray = []; + for (let i = 0; i < this.length; ++i) { + res.push(this.params[i]); + const start = this._subParamsIdx[i] >> 8; + const end = this._subParamsIdx[i] & 0xFF; + if (end - start > 0) { + res.push(Array.prototype.slice.call(this._subParams, start, end)); + } + } + return res; + } + + /** + * Reset to initial empty state. + */ + public reset(): void { + this.length = 0; + this._subParamsLength = 0; + this._rejectDigits = false; + this._rejectSubDigits = false; + } + + /** + * Add a parameter value. + * `Params` only stores up to `maxLength` parameters, any later + * parameter will be ignored. + * Note: VT devices only stored up to 16 values, xterm seems to + * store up to 30. + */ + public addParam(value: number): void { + if (this.length >= this.maxLength) { + this._rejectDigits = true; + return; + } + if (value < -1) { + throw new Error('values lesser than -1 are not allowed'); + } + this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength; + this.params[this.length++] = value > MAX_VALUE ? MAX_VALUE : value; + } + + /** + * Add a sub parameter value. + * The sub parameter is automatically associated with the last parameter value. + * Thus it is not possible to add a subparameter without any parameter added yet. + * `Params` only stores up to `subParamsLength` sub parameters, any later + * sub parameter will be ignored. + */ + public addSubParam(value: number): void { + if (!this.length) { + return; + } + if (this._subParamsLength >= this.maxSubParamsLength) { + this._rejectSubDigits = true; + return; + } + if (value < -1) { + throw new Error('values lesser than -1 are not allowed'); + } + this._subParams[this._subParamsLength++] = value > MAX_VALUE ? MAX_VALUE : value; + this._subParamsIdx[this.length - 1]++; + } + + /** + * Whether parameter at index `idx` has sub parameters. + */ + public hasSubParams(idx: number): boolean { + return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0); + } + + /** + * Return sub parameters for parameter at index `idx`. + * Note: The values are borrowed, thus you need to copy + * the values if you need to hold them in nonlocal scope. + */ + public getSubParams(idx: number): Int32Array | null { + const start = this._subParamsIdx[idx] >> 8; + const end = this._subParamsIdx[idx] & 0xFF; + if (end - start > 0) { + return this._subParams.subarray(start, end); + } + return null; + } + + /** + * Return all sub parameters as {idx: subparams} mapping. + * Note: The values are not borrowed. + */ + public getSubParamsAll(): {[idx: number]: Int32Array} { + const result: {[idx: number]: Int32Array} = {}; + for (let i = 0; i < this.length; ++i) { + const start = this._subParamsIdx[i] >> 8; + const end = this._subParamsIdx[i] & 0xFF; + if (end - start > 0) { + result[i] = this._subParams.slice(start, end); + } + } + return result; + } + + /** + * Add a single digit value to current parameter. + * This is used by the parser to account digits on a char by char basis. + * Do not use this method directly, consider using `addParam` instead. + */ + public addParamDigit(value: number): void { + if (this._rejectDigits) { + return; + } + const v = this.params[this.length - 1] * 10 + value; + this.params[this.length - 1] = v > MAX_VALUE ? MAX_VALUE : v; + } + + /** + * Add a single digit value to current sub parameter. + * This is used by the parser to account digits on a char by char basis. + * Do not use this method directly, consider using `addSubParam` instead. + */ + public addSubParamDigit(value: number): void { + if (!this._subParamsLength || this._rejectDigits || this._rejectSubDigits) { + return; + } + if (this._subParams[this._subParamsLength - 1] === -1) { + this._subParams[this._subParamsLength - 1] = value; + } else { + const v = this._subParams[this._subParamsLength - 1] * 10 + value; + this._subParams[this._subParamsLength - 1] = v > MAX_VALUE ? MAX_VALUE : v; + } + } +} diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 47ec98e1..d432800d 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -6,6 +6,38 @@ import { IDisposable } from 'common/Types'; import { ParserState } from 'common/parser/Constants'; +/** sequence params serialized to js arrays */ +export type ParamsArray = (number | number[])[]; + +/** Params constructor type. */ +export interface IParamsConstructor { + new(maxLength: number, maxSubParamsLength: number): IParams; + + /** create params from ParamsArray */ + fromArray(values: ParamsArray): IParams; +} + +/** Interface of Params storage class. */ +export interface IParams { + /** from ctor */ + maxLength: number; + maxSubParamsLength: number; + + /** param values and its length */ + params: Int32Array; + length: number; + + /** methods */ + clone(): IParams; + toArray(): ParamsArray; + reset(): void; + addParam(value: number): void; + addSubParam(value: number): void; + hasSubParams(idx: number): boolean; + getSubParams(idx: number): Int32Array | null; + getSubParamsAll(): {[idx: number]: Int32Array}; +} + /** * Internal state of EscapeSequenceParser. * Used as argument of the error handler to allow @@ -26,7 +58,7 @@ export interface IParsingState { // collect buffer with intermediate characters collect: string; // params buffer - params: number[]; + params: IParams; // should abort (default: false) abort: boolean; } @@ -53,7 +85,7 @@ export interface IParsingState { * `unhook` marks the end of the current DCS sequence. */ export interface IDcsHandler { - hook(collect: string, params: number[], flag: number): void; + hook(collect: string, params: IParams, flag: number): void; put(data: Uint32Array, start: number, end: number): void; unhook(): void; } @@ -68,13 +100,14 @@ export interface IEscapeSequenceParser extends IDisposable { * It gets reset by the parser for any valid sequence beside REP itself. */ precedingCodepoint: number; + /** * Reset the parser to its initial state (handlers are kept). */ reset(): void; /** - * Parse string `data`. + * Parse UTF32 codepoints in `data` up to `length`. * @param data The data to parse. */ parse(data: Uint32Array, length: number): void; @@ -86,10 +119,10 @@ export interface IEscapeSequenceParser extends IDisposable { clearExecuteHandler(flag: string): void; setExecuteHandlerFallback(callback: (code: number) => void): void; - setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; + setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void; clearCsiHandler(flag: string): void; - setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 646d44a2..f06d003a 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -11,6 +11,7 @@ import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; +import { IParams } from 'common/parser/Types'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -56,8 +57,8 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { - return this._core.addCsiHandler(flag, callback); + public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable { + return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect)); } public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { return this._core.addOscHandler(ident, callback); diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index cbf5dbd6..d9c9bf89 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -309,6 +309,23 @@ describe('InputHandler Integration Tests', function(): void { }); }); }); + + describe('addCsiHandler', () => { + it('should call custom CSI handler with js array params', async () => { + await page.evaluate(` + window.term.reset(); + const _customCsiHandlerParams = []; + const _customCsiHandler = window.term.addCsiHandler('m', (params, collect) => { + _customCsiHandlerParams.push(params); + return false; + }, ''); + `); + await page.evaluate(` + window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); + `); + assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index f92afc80..b9a6b0df 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -6,7 +6,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { C0, C1 } from 'common/data/EscapeSequences'; -import { IDcsHandler } from 'common/parser/Types'; +import { IDcsHandler, IParams } from 'common/parser/Types'; function toUtf32(s: string): Uint32Array { @@ -18,7 +18,7 @@ function toUtf32(s: string): Uint32Array { } class DcsHandler implements IDcsHandler { - hook(collect: string, params: number[], flag: number) : void {} + hook(collect: string, params: IParams, flag: number) : void {} put(data: Uint32Array, start: number, end: number) : void {} unhook() :void {} } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6ef10b62..c915a4ba 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -489,12 +489,14 @@ declare module 'xterm' { * final character (e.g "m" for SGR) of the CSI sequence. * @param callback The function to handle the escape sequence. The callback * is called with the numerical params, as well as the special characters - * (e.g. "$" for DECSCPP). Return true if the sequence was handled; false if + * (e.g. "$" for DECSCPP). If the sequence has subparams the array will + * contain subarrays with their numercial values. + * Return true if the sequence was handled; false if * we should try a previous handler (set by addCsiHandler or setCsiHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; /** * (EXPERIMENTAL) Adds a handler for OSC escape sequences.