diff --git a/demo/server.js b/demo/server.js index 3b170813b9..8955431b1a 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 e93fc945d6..b8f94d5bff 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,4 +547,154 @@ 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); + }); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7250bdb538..577029f84c 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 } 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()); @@ -484,7 +484,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); } @@ -579,10 +579,10 @@ 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 { 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); @@ -592,11 +592,8 @@ 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; - } + public cursorUp(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.y -= param; if (this._terminal.buffer.y < 0) { this._terminal.buffer.y = 0; @@ -607,11 +604,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps B * Cursor Down Ps Times (default = 1) (CUD). */ - public cursorDown(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public cursorDown(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.y += param; if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; @@ -626,11 +620,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps C * Cursor Forward Ps Times (default = 1) (CUF). */ - public cursorForward(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public cursorForward(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.x += param; if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; @@ -641,11 +632,9 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps D * Cursor Backward Ps Times (default = 1) (CUB). */ - public cursorBackward(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public cursorBackward(params: IParams): void { + const param = params.params[0] || 1; + // If the end of the line is hit, prevent this action from wrapping around to the next line. if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x--; @@ -661,11 +650,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Next Line Ps Times (default = 1) (CNL). * same as CSI Ps B ? */ - public cursorNextLine(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public cursorNextLine(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.y += param; if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; @@ -679,11 +665,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Preceding Line Ps Times (default = 1) (CNL). * reuse CSI Ps A ? */ - public cursorPrecedingLine(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public cursorPrecedingLine(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.y -= param; if (this._terminal.buffer.y < 0) { this._terminal.buffer.y = 0; @@ -696,11 +679,8 @@ 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; - } + public cursorCharAbsolute(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.x = param - 1; } @@ -708,15 +688,9 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps ; Ps H * Cursor Position [row;column] (default = [1,1]) (CUP). */ - public cursorPosition(params: number[]): void { - let col: number; - let row: number = params[0] - 1; - - if (params.length >= 2) { - col = params[1] - 1; - } else { - col = 0; - } + public cursorPosition(params: IParams): void { + let row: number = params.params[0] - 1; + let col: number = (params.length >= 2) ? params.params[1] - 1 : 0; if (row < 0) { row = 0; @@ -738,8 +712,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps I * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). */ - public cursorForwardTab(params: number[]): void { - let param = params[0] || 1; + public cursorForwardTab(params: IParams): void { + let param = params.params[0] || 1; while (param--) { this._terminal.buffer.x = this._terminal.buffer.nextStop(); } @@ -785,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); @@ -844,8 +818,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase to Left. * Ps = 2 -> Selective Erase All. */ - public eraseInLine(params: number[]): void { - switch (params[0]) { + public eraseInLine(params: IParams): void { + switch (params.params[0]) { case 0: this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols); break; @@ -863,11 +837,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; @@ -892,11 +863,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; @@ -922,10 +890,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); @@ -934,8 +902,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; @@ -952,9 +920,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; @@ -973,10 +941,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); @@ -985,8 +953,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; @@ -1000,11 +968,8 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Pm ` Character Position Absolute * [column] (default = [row,1]) (HPA). */ - public charPosAbsolute(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public charPosAbsolute(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.x = param - 1; if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; @@ -1016,11 +981,8 @@ export class InputHandler extends Disposable implements IInputHandler { * [columns] (default = [row,col+1]) (HPR) * reuse CSI Ps C ? */ - public hPositionRelative(params: number[]): void { - let param = params[0]; - if (param < 1) { - param = 1; - } + public hPositionRelative(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.x += param; if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; @@ -1049,12 +1011,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; @@ -1099,8 +1061,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; } @@ -1121,7 +1083,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'); } @@ -1132,11 +1094,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; - } + public linePosAbsolute(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.y = param - 1; if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; @@ -1148,11 +1107,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; - } + public vPositionRelative(params: IParams): void { + const param = params.params[0] || 1; this._terminal.buffer.y += param; if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; @@ -1168,16 +1124,16 @@ export class InputHandler extends Disposable implements IInputHandler { * Horizontal and Vertical Position [row;column] (default = * [1,1]) (HVP). */ - public hVPosition(params: number[]): void { - if (params[0] < 1) params[0] = 1; - if (params[1] < 1) params[1] = 1; + public hVPosition(params: IParams): void { + const row = params.params[0] || 1; + const col = (params.length > 1) ? params.params[1] || 1 : 1; - this._terminal.buffer.y = params[0] - 1; + this._terminal.buffer.y = row - 1; if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; } - this._terminal.buffer.x = params[1] - 1; + this._terminal.buffer.x = col - 1; if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } @@ -1191,9 +1147,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 = {}; @@ -1286,17 +1242,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; @@ -1305,7 +1259,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; @@ -1348,9 +1302,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'); @@ -1388,10 +1342,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 @@ -1491,17 +1445,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; @@ -1510,7 +1462,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; @@ -1567,7 +1519,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 @@ -1575,8 +1527,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) { @@ -1591,6 +1543,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). @@ -1656,9 +1680,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; @@ -1669,7 +1693,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); @@ -1739,34 +1763,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); @@ -1802,9 +1801,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`); @@ -1819,7 +1818,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; @@ -1850,7 +1849,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; @@ -1881,9 +1880,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: @@ -1908,13 +1907,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; } @@ -1925,7 +1925,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; @@ -1938,7 +1938,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; diff --git a/src/Terminal.ts b/src/Terminal.ts index 902c6d5162..ccec42e8b0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -56,6 +56,7 @@ 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. @@ -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. */ diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index b203d1a140..62ae609e96 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 a54606a84a..ee6815dae6 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; @@ -239,7 +240,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; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index f445efc403..90ec0ee0f9 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,17 +567,29 @@ 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]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0, 0]); + 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); @@ -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 d8d4e02eda..364ba803d7 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; @@ -497,11 +506,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } 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: @@ -515,7 +536,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: @@ -544,7 +566,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } if (code === 0x1b) transition |= ParserState.ESCAPE; osc = ''; - params = [0]; + params.reset(); + params.addParam(0); // ZDM collect = ''; break; case ParserAction.OSC_START: @@ -590,7 +613,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } if (code === 0x1b) transition |= ParserState.ESCAPE; osc = ''; - params = [0]; + params.reset(); + params.addParam(0); // ZDM collect = ''; break; } diff --git a/src/common/parser/Params.test.ts b/src/common/parser/Params.test.ts new file mode 100644 index 0000000000..01287b8f7a --- /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 0000000000..7ef8341a72 --- /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 47ec98e15d..d432800d58 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 646d44a2de..f06d003aa3 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 cbf5dbd69c..d9c9bf89cc 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 f92afc8054..b9a6b0df66 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 6ef10b629c..c915a4ba71 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.