diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 38645a0b..8752e0a3 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -53,39 +53,38 @@ describe('InputHandler', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService); - const collect = ' '; - inputHandler.setCursorStyle(Params.fromArray([0]), collect); + inputHandler.setCursorStyle(Params.fromArray([0])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([1]), collect); + inputHandler.setCursorStyle(Params.fromArray([1])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([2]), collect); + inputHandler.setCursorStyle(Params.fromArray([2])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], false); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([3]), collect); + inputHandler.setCursorStyle(Params.fromArray([3])); assert.equal(optionsService.options['cursorStyle'], 'underline'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([4]), collect); + inputHandler.setCursorStyle(Params.fromArray([4])); assert.equal(optionsService.options['cursorStyle'], 'underline'); assert.equal(optionsService.options['cursorBlink'], false); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([5]), collect); + inputHandler.setCursorStyle(Params.fromArray([5])); assert.equal(optionsService.options['cursorStyle'], 'bar'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([6]), collect); + inputHandler.setCursorStyle(Params.fromArray([6])); assert.equal(optionsService.options['cursorStyle'], 'bar'); assert.equal(optionsService.options['cursorBlink'], false); }); @@ -93,14 +92,13 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle Terminal.bracketedPasteMode', () => { const terminal = new MockInputHandlingTerminal(); - const collect = '?'; terminal.bracketedPasteMode = false; const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // Set bracketed paste mode - inputHandler.setMode(Params.fromArray([2004]), collect); + inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, true); // Reset bracketed paste mode - inputHandler.resetMode(Params.fromArray([2004]), collect); + inputHandler.resetModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, false); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 62d95cdc..02e20961 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,7 +14,7 @@ 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, IParams } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -50,7 +50,7 @@ class DECRQSS implements IDcsHandler { private _optionsService: IOptionsService ) { } - hook(collect: string, params: IParams, flag: number): void { + hook(params: IParams): void { this._data = new Uint32Array(0); } @@ -149,69 +149,74 @@ export class InputHandler extends Disposable implements IInputHandler { /** * custom fallback handlers */ - this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - this._logService.debug('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) }); + this._parser.setCsiHandlerFallback((ident, params) => { + this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() }); }); - this._parser.setEscHandlerFallback((collect: string, flag: number) => { - this._logService.debug('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) }); + this._parser.setEscHandlerFallback(ident => { + this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) }); }); - this._parser.setExecuteHandlerFallback((code: number) => { + this._parser.setExecuteHandlerFallback(code => { this._logService.debug('Unknown EXECUTE code: ', { code }); }); this._parser.setOscHandlerFallback((identifier, action, data) => { this._logService.debug('Unknown OSC code: ', { identifier, action, data }); }); - this._parser.setDcsHandlerFallback((identifier, action, payload) => { - if (payload.params) { - payload.params = payload.params.toArray(); + this._parser.setDcsHandlerFallback((ident, action, payload) => { + if (action === 'HOOK') { + payload = payload.toArray(); } - this._logService.debug('Unknown DCS code: ', { identifier, action, payload }); + this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload }); }); /** * print handler */ - this._parser.setPrintHandler((data, start, end): void => this.print(data, start, end)); + this._parser.setPrintHandler((data, start, end) => this.print(data, start, end)); /** * CSI handler */ - this._parser.setCsiHandler('@', (params, collect) => this.insertChars(params)); - this._parser.setCsiHandler('A', (params, collect) => this.cursorUp(params)); - this._parser.setCsiHandler('B', (params, collect) => this.cursorDown(params)); - this._parser.setCsiHandler('C', (params, collect) => this.cursorForward(params)); - this._parser.setCsiHandler('D', (params, collect) => this.cursorBackward(params)); - this._parser.setCsiHandler('E', (params, collect) => this.cursorNextLine(params)); - this._parser.setCsiHandler('F', (params, collect) => this.cursorPrecedingLine(params)); - this._parser.setCsiHandler('G', (params, collect) => this.cursorCharAbsolute(params)); - this._parser.setCsiHandler('H', (params, collect) => this.cursorPosition(params)); - this._parser.setCsiHandler('I', (params, collect) => this.cursorForwardTab(params)); - this._parser.setCsiHandler('J', (params, collect) => this.eraseInDisplay(params)); - this._parser.setCsiHandler('K', (params, collect) => this.eraseInLine(params)); - this._parser.setCsiHandler('L', (params, collect) => this.insertLines(params)); - this._parser.setCsiHandler('M', (params, collect) => this.deleteLines(params)); - this._parser.setCsiHandler('P', (params, collect) => this.deleteChars(params)); - this._parser.setCsiHandler('S', (params, collect) => this.scrollUp(params)); - this._parser.setCsiHandler('T', (params, collect) => this.scrollDown(params, collect)); - this._parser.setCsiHandler('X', (params, collect) => this.eraseChars(params)); - this._parser.setCsiHandler('Z', (params, collect) => this.cursorBackwardTab(params)); - this._parser.setCsiHandler('`', (params, collect) => this.charPosAbsolute(params)); - this._parser.setCsiHandler('a', (params, collect) => this.hPositionRelative(params)); - this._parser.setCsiHandler('b', (params, collect) => this.repeatPrecedingCharacter(params)); - this._parser.setCsiHandler('c', (params, collect) => this.sendDeviceAttributes(params, collect)); - this._parser.setCsiHandler('d', (params, collect) => this.linePosAbsolute(params)); - this._parser.setCsiHandler('e', (params, collect) => this.vPositionRelative(params)); - this._parser.setCsiHandler('f', (params, collect) => this.hVPosition(params)); - this._parser.setCsiHandler('g', (params, collect) => this.tabClear(params)); - this._parser.setCsiHandler('h', (params, collect) => this.setMode(params, collect)); - this._parser.setCsiHandler('l', (params, collect) => this.resetMode(params, collect)); - this._parser.setCsiHandler('m', (params, collect) => this.charAttributes(params)); - this._parser.setCsiHandler('n', (params, collect) => this.deviceStatus(params, collect)); - this._parser.setCsiHandler('p', (params, collect) => this.softReset(params, collect)); - this._parser.setCsiHandler('q', (params, collect) => this.setCursorStyle(params, collect)); - this._parser.setCsiHandler('r', (params, collect) => this.setScrollRegion(params, collect)); - this._parser.setCsiHandler('s', (params, collect) => this.saveCursor(params)); - this._parser.setCsiHandler('u', (params, collect) => this.restoreCursor(params)); + this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params)); + this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params)); + this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params)); + this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params)); + this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params)); + this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params)); + this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params)); + this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params)); + this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params)); + this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params)); + this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params)); + this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params)); + this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params)); + this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params)); + this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params)); + this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params)); + this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params)); + this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params)); + this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params)); + this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params)); + this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params)); + this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params)); + this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params)); + this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params)); + this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params)); + this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params)); + this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params)); + this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params)); + this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params)); + this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params)); + this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params)); + this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params)); + this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params)); + this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params)); + this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params)); + this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params)); + this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params)); + this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params)); + this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params)); + this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); + this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); /** * execute handler @@ -276,32 +281,32 @@ export class InputHandler extends Disposable implements IInputHandler { /** * ESC handlers */ - 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()); - this._parser.setEscHandler('M', () => this.reverseIndex()); - this._parser.setEscHandler('=', () => this.keypadApplicationMode()); - this._parser.setEscHandler('>', () => this.keypadNumericMode()); - this._parser.setEscHandler('c', () => this.reset()); - this._parser.setEscHandler('n', () => this.setgLevel(2)); - this._parser.setEscHandler('o', () => this.setgLevel(3)); - this._parser.setEscHandler('|', () => this.setgLevel(3)); - this._parser.setEscHandler('}', () => this.setgLevel(2)); - this._parser.setEscHandler('~', () => this.setgLevel(1)); - this._parser.setEscHandler('%@', () => this.selectDefaultCharset()); - this._parser.setEscHandler('%G', () => this.selectDefaultCharset()); + this._parser.setEscHandler({final: '7'}, () => this.saveCursor()); + this._parser.setEscHandler({final: '8'}, () => this.restoreCursor()); + this._parser.setEscHandler({final: 'D'}, () => this.index()); + this._parser.setEscHandler({final: 'E'}, () => this.nextLine()); + this._parser.setEscHandler({final: 'H'}, () => this.tabSet()); + this._parser.setEscHandler({final: 'M'}, () => this.reverseIndex()); + this._parser.setEscHandler({final: '='}, () => this.keypadApplicationMode()); + this._parser.setEscHandler({final: '>'}, () => this.keypadNumericMode()); + this._parser.setEscHandler({final: 'c'}, () => this.reset()); + this._parser.setEscHandler({final: 'n'}, () => this.setgLevel(2)); + this._parser.setEscHandler({final: 'o'}, () => this.setgLevel(3)); + this._parser.setEscHandler({final: '|'}, () => this.setgLevel(3)); + this._parser.setEscHandler({final: '}'}, () => this.setgLevel(2)); + this._parser.setEscHandler({final: '~'}, () => this.setgLevel(1)); + this._parser.setEscHandler({intermediates: '%', final: '@'}, () => this.selectDefaultCharset()); + this._parser.setEscHandler({intermediates: '%', final: 'G'}, () => this.selectDefaultCharset()); for (const flag in CHARSETS) { - this._parser.setEscHandler('(' + flag, () => this.selectCharset('(' + flag)); - this._parser.setEscHandler(')' + flag, () => this.selectCharset(')' + flag)); - this._parser.setEscHandler('*' + flag, () => this.selectCharset('*' + flag)); - this._parser.setEscHandler('+' + flag, () => this.selectCharset('+' + flag)); - this._parser.setEscHandler('-' + flag, () => this.selectCharset('-' + flag)); - this._parser.setEscHandler('.' + flag, () => this.selectCharset('.' + flag)); - this._parser.setEscHandler('/' + flag, () => this.selectCharset('/' + flag)); // TODO: supported? + this._parser.setEscHandler({intermediates: '(', final: flag}, () => this.selectCharset('(' + flag)); + this._parser.setEscHandler({intermediates: ')', final: flag}, () => this.selectCharset(')' + flag)); + this._parser.setEscHandler({intermediates: '*', final: flag}, () => this.selectCharset('*' + flag)); + this._parser.setEscHandler({intermediates: '+', final: flag}, () => this.selectCharset('+' + flag)); + this._parser.setEscHandler({intermediates: '-', final: flag}, () => this.selectCharset('-' + flag)); + this._parser.setEscHandler({intermediates: '.', final: flag}, () => this.selectCharset('.' + flag)); + this._parser.setEscHandler({intermediates: '/', final: flag}, () => this.selectCharset('/' + flag)); // TODO: supported? } - this._parser.setEscHandler('#8', () => this.screenAlignmentPattern()); + this._parser.setEscHandler({intermediates: '#', final: '8'}, () => this.screenAlignmentPattern()); /** * error handler @@ -314,7 +319,7 @@ export class InputHandler extends Disposable implements IInputHandler { /** * DCS handler */ - this._parser.setDcsHandler('$q', new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); + this._parser.setDcsHandler({intermediates: '$', final: 'q'}, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); } public dispose(): void { @@ -489,22 +494,22 @@ export class InputHandler extends Disposable implements IInputHandler { /** * Forward addCsiHandler from parser. */ - public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - return this._parser.addCsiHandler(flag, callback); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._parser.addCsiHandler(id, callback); } /** * Forward addDcsHandler from parser. */ - public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { - return this._parser.addDcsHandler(collectAndFlag, new DcsHandlerFactory(callback)); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._parser.addDcsHandler(id, new DcsHandlerFactory(callback)); } /** * Forward addEscHandler from parser. */ - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._parser.addEscHandler(collectAndFlag, handler); + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._parser.addEscHandler(id, callback); } /** @@ -1030,8 +1035,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: IParams, collect?: string): void { - if (params.length < 2 && !collect) { + public scrollDown(params: IParams): void { + if (params.length < 2) { let param = params.params[0] || 1; // make buffer local for faster access @@ -1134,32 +1139,33 @@ 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: IParams, collect?: string): void { + public sendDeviceAttributesPrimary(params: IParams): void { if (params.params[0] > 0) { return; } - - if (!collect) { - if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { - this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); - } else if (this._terminal.is('linux')) { - this._coreService.triggerDataEvent(C0.ESC + '[?6c'); - } - } else if (collect === '>') { - // xterm and urxvt - // seem to spit this - // out around ~370 times (?). - if (this._terminal.is('xterm')) { - this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); - } else if (this._terminal.is('rxvt-unicode')) { - this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); - } else if (this._terminal.is('linux')) { - // not supported by linux console. - // linux console echoes parameters. - this._coreService.triggerDataEvent(params.params[0] + 'c'); - } else if (this._terminal.is('screen')) { - this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); - } + if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); + } else if (this._terminal.is('linux')) { + this._coreService.triggerDataEvent(C0.ESC + '[?6c'); + } + } + public sendDeviceAttributesSecondary(params: IParams): void { + if (params.params[0] > 0) { + return; + } + // xterm and urxvt + // seem to spit this + // out around ~370 times (?). + if (this._terminal.is('xterm')) { + this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); + } else if (this._terminal.is('rxvt-unicode')) { + this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); + } else if (this._terminal.is('linux')) { + // not supported by linux console. + // linux console echoes parameters. + this._coreService.triggerDataEvent(params.params[0] + 'c'); + } else if (this._terminal.is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } } @@ -1249,15 +1255,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: IParams, collect?: string): void { + public setMode(params: IParams): 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 (param) { + switch (params.params[i]) { case 4: this._terminal.insertMode = true; break; @@ -1265,8 +1265,11 @@ export class InputHandler extends Disposable implements IInputHandler { // this._t.convertEol = true; break; } - } else if (collect === '?') { - switch (param) { + } + } + public setModePrivate(params: IParams): void { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = true; break; @@ -1312,9 +1315,9 @@ export class InputHandler extends Disposable implements IInputHandler { // TODO: Why are params[0] compares nested within a switch for params[0]? - this._terminal.x10Mouse = param === 9; - this._terminal.vt200Mouse = param === 1000; - this._terminal.normalMouse = param > 1000; + this._terminal.x10Mouse = params.params[i] === 9; + this._terminal.vt200Mouse = params.params[i] === 1000; + this._terminal.normalMouse = params.params[i] > 1000; this._terminal.mouseEvents = true; if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); @@ -1373,6 +1376,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** * CSI Pm l Reset Mode (RM). * Ps = 2 -> Keyboard Action Mode (AM). @@ -1455,15 +1459,9 @@ 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: IParams, collect?: string): void { + public resetMode(params: IParams): 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 (param) { + switch (params.params[i]) { case 4: this._terminal.insertMode = false; break; @@ -1471,8 +1469,11 @@ export class InputHandler extends Disposable implements IInputHandler { // this._t.convertEol = false; break; } - } else if (collect === '?') { - switch (param) { + } + } + public resetModePrivate(params: IParams): void { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = false; break; @@ -1542,7 +1543,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 1047: // normal screen buffer - clearing it first // Ensure the selection manager has the correct buffer this._bufferService.buffers.activateNormalBuffer(); - if (param === 1049) { + if (params.params[i] === 1049) { this.restoreCursor(); } this._terminal.refresh(0, this._bufferService.rows - 1); @@ -1558,6 +1559,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** * Helper to extract and apply color params/subparams. * Returns advance for params index. @@ -1816,47 +1818,46 @@ 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: IParams, collect?: string): void { - if (!collect) { - switch (params.params[0]) { - case 5: - // status report - this._coreService.triggerDataEvent(`${C0.ESC}[0n`); - break; - case 6: - // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; - this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); - break; - } - } else if (collect === '?') { - // modern xterm doesnt seem to - // respond to any of these except ?6, 6, and 5 - switch (params.params[0]) { - case 6: - // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; - this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); - break; - case 15: - // no printer - // this.handler(C0.ESC + '[?11n'); - break; - case 25: - // dont support user defined keys - // this.handler(C0.ESC + '[?21n'); - break; - case 26: - // north american keyboard - // this.handler(C0.ESC + '[?27;1;0;0n'); - break; - case 53: - // no dec locator/mouse - // this.handler(C0.ESC + '[?50n'); - break; - } + public deviceStatus(params: IParams): void { + switch (params.params[0]) { + case 5: + // status report + this._coreService.triggerDataEvent(`${C0.ESC}[0n`); + break; + case 6: + // cursor position + const y = this._bufferService.buffer.y + 1; + const x = this._bufferService.buffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); + break; + } + } + public deviceStatusPrivate(params: IParams): void { + // modern xterm doesnt seem to + // respond to any of these except ?6, 6, and 5 + switch (params.params[0]) { + case 6: + // cursor position + const y = this._bufferService.buffer.y + 1; + const x = this._bufferService.buffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); + break; + case 15: + // no printer + // this.handler(C0.ESC + '[?11n'); + break; + case 25: + // dont support user defined keys + // this.handler(C0.ESC + '[?21n'); + break; + case 26: + // north american keyboard + // this.handler(C0.ESC + '[?27;1;0;0n'); + break; + case 53: + // no dec locator/mouse + // this.handler(C0.ESC + '[?50n'); + break; } } @@ -1864,25 +1865,23 @@ 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: IParams, collect?: string): void { - if (collect === '!') { - this._terminal.cursorHidden = false; - this._terminal.insertMode = false; - this._terminal.originMode = false; - this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false - this._terminal.applicationKeypad = false; // ? - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } - this._coreService.decPrivateModes.applicationCursorKeys = false; - this._bufferService.buffer.scrollTop = 0; - this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; - this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); - this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + public softReset(params: IParams): void { + this._terminal.cursorHidden = false; + this._terminal.insertMode = false; + this._terminal.originMode = false; + this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false + this._terminal.applicationKeypad = false; // ? + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); } + this._coreService.decPrivateModes.applicationCursorKeys = false; + this._bufferService.buffer.scrollTop = 0; + this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; + this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? + this._terminal.charset = null; + this._terminal.glevel = 0; // ?? + this._terminal.charsets = [null]; // ?? } /** @@ -1895,40 +1894,32 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: IParams, collect?: string): void { - if (collect === ' ') { - const param = params.params[0] || 1; - switch (param) { - case 1: - case 2: - this._optionsService.options.cursorStyle = 'block'; - break; - case 3: - case 4: - this._optionsService.options.cursorStyle = 'underline'; - break; - case 5: - case 6: - this._optionsService.options.cursorStyle = 'bar'; - break; - } - const isBlinking = param % 2 === 1; - this._optionsService.options.cursorBlink = isBlinking; + public setCursorStyle(params: IParams): void { + const param = params.params[0] || 1; + switch (param) { + case 1: + case 2: + this._optionsService.options.cursorStyle = 'block'; + break; + case 3: + case 4: + this._optionsService.options.cursorStyle = 'underline'; + break; + case 5: + case 6: + this._optionsService.options.cursorStyle = 'bar'; + break; } + const isBlinking = param % 2 === 1; + this._optionsService.options.cursorBlink = isBlinking; } /** * CSI Ps ; Ps r * Set Scrolling Region [top;bottom] (default = full size of win- * dow) (DECSTBM). - * CSI ? Pm r - * currently skipped */ - public setScrollRegion(params: IParams, collect?: string): void { - if (collect) { - return; - } - + public setScrollRegion(params: IParams): void { const top = params.params[0] || 1; let bottom: number; diff --git a/src/Terminal.ts b/src/Terminal.ts index b0dedf33..8dd684a2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -55,7 +55,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 { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; import { LogService } from 'common/services/LogService'; import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; @@ -1395,18 +1395,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._inputHandler.addEscHandler(collectAndFlag, handler); + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._inputHandler.addEscHandler(id, callback); } /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ - public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { - return this._inputHandler.addDcsHandler(collectAndFlag, callback); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(id, callback); } /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - return this._inputHandler.addCsiHandler(flag, callback); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(id, callback); } /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 6552c3e3..7d03126d 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -15,7 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { @@ -74,13 +74,13 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { throw new Error('Method not implemented.'); } - addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { throw new Error('Method not implemented.'); } - addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + addEscHandler(id: IFunctionIdentifier, handler: () => 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 e6e540a3..e3a9771d 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -9,7 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -113,7 +113,8 @@ export interface IInputHandler { /** 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 c */ sendDeviceAttributesPrimary(params: IParams): void; + sendDeviceAttributesSecondary(params: IParams): void; /** CSI d */ linePosAbsolute(params: IParams): void; /** CSI e */ vPositionRelative(params: IParams): void; /** CSI f */ hVPosition(params: IParams): void; @@ -200,9 +201,9 @@ export interface IPublicTerminal extends IDisposable { writeln(data: string): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; - addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable; - addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, callback: () => 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/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index 172f51bc..3ef42a68 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; import { DcsParser, DcsHandlerFactory } from 'common/parser/DcsParser'; -import { IDcsHandler, IParams } from 'common/parser/Types'; +import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -16,10 +16,47 @@ function toUtf32(s: string): Uint32Array { return utf32.subarray(0, length); } +function identifier(id: IFunctionIdentifier): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (0x40 > finalCode || finalCode > 0x7e) { + throw new Error('final must be in range 0x40 .. 0x7e'); + } + res <<= 8; + res |= finalCode; + + return res; +} + class TestHandler implements IDcsHandler { constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} - hook(collect: string, params: IParams, flag: number): void { - this.output.push([this.msg, 'HOOK', params.toArray(), collect, flag]); + hook(params: IParams): void { + this.output.push([this.msg, 'HOOK', params.toArray()]); } put(data: Uint32Array, start: number, end: number): void { this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); @@ -39,16 +76,16 @@ describe('DcsParser', () => { reports = []; parser = new DcsParser(); parser.setDcsHandlerFallback((id, action, data) => { - if (data.params) { - data.params = data.params.toArray(); + if (action === 'HOOK') { + data = data.toArray(); } reports.push([id, action, data]); }); }); describe('handler registration', () => { it('setDcsHandler', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th')); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); @@ -56,16 +93,16 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [ // messages from TestHandler - ['th', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th', 'HOOK', [1, 2, 3]], ['th', 'PUT', 'Here comes'], ['th', 'PUT', 'the mouse!'], ['th', 'UNHOOK', true] ]); }); it('clearDcsHandler', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th')); - parser.clearDcsHandler('+p'); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.clearDcsHandler(identifier({intermediates: '+', final: 'p'})); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); @@ -73,24 +110,24 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [ // messages from fallback handler - ['+p', 'HOOK', {collect: '+', params: [1, 2, 3], flag: 'p'.charCodeAt(0)}], - ['+p', 'PUT', 'Here comes'], - ['+p', 'PUT', 'the mouse!'], - ['+p', 'UNHOOK', true] + [identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]], + [identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'], + [identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'], + [identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true] ]); }); it('addDcsHandler', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); - parser.addDcsHandler('+p', new TestHandler(reports, 'th2')); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); parser.put(data, 0, data.length); parser.unhook(true); assert.deepEqual(reports, [ - ['th2', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], - ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th2', 'HOOK', [1, 2, 3]], + ['th1', 'HOOK', [1, 2, 3]], ['th2', 'PUT', 'Here comes'], ['th1', 'PUT', 'Here comes'], ['th2', 'PUT', 'the mouse!'], @@ -100,17 +137,17 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler with return false', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); - parser.addDcsHandler('+p', new TestHandler(reports, 'th2', true)); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); parser.put(data, 0, data.length); parser.unhook(true); assert.deepEqual(reports, [ - ['th2', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], - ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th2', 'HOOK', [1, 2, 3]], + ['th1', 'HOOK', [1, 2, 3]], ['th2', 'PUT', 'Here comes'], ['th1', 'PUT', 'Here comes'], ['th2', 'PUT', 'the mouse!'], @@ -120,17 +157,17 @@ describe('DcsParser', () => { ]); }); it('dispose handlers', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); - const dispo = parser.addDcsHandler('+p', new TestHandler(reports, 'th2', true)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); dispo.dispose(); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); parser.put(data, 0, data.length); parser.unhook(true); assert.deepEqual(reports, [ - ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th1', 'HOOK', [1, 2, 3]], ['th1', 'PUT', 'Here comes'], ['th1', 'PUT', 'the mouse!'], ['th1', 'UNHOOK', true] @@ -139,8 +176,8 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -149,8 +186,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -159,9 +196,9 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); - const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['two', params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -169,7 +206,7 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); dispo.dispose(); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); data = toUtf32('some other'); parser.put(data, 0, data.length); data = toUtf32(' data'); @@ -178,9 +215,9 @@ describe('DcsParser', () => { assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); it('should respect return false', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); - parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { reports.push(['two', params.toArray(), data]); return false; })); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -190,8 +227,8 @@ describe('DcsParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); const data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { parser.put(data, 0, data.length); @@ -201,8 +238,8 @@ describe('DcsParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { parser.put(data, 0, data.length); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index f89edcfc..a22ca696 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -13,7 +13,7 @@ import { PAYLOAD_LIMIT } from 'common/parser/Constants'; export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); private _active: IDcsHandler[] = []; - private _collectAndFlag: string = ''; + private _ident: number = 0; private _handlerFb: DcsFallbackHandler = () => {}; public dispose(): void { @@ -21,11 +21,11 @@ export class DcsParser implements IDcsParser { this._handlerFb = () => {}; } - public addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { - if (this._handlers[collectAndFlag] === undefined) { - this._handlers[collectAndFlag] = []; + public addDcsHandler(ident: number, handler: IDcsHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; } - const handlerList = this._handlers[collectAndFlag]; + const handlerList = this._handlers[ident]; handlerList.push(handler); return { dispose: () => { @@ -37,12 +37,12 @@ export class DcsParser implements IDcsParser { }; } - public setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._handlers[collectAndFlag] = [handler]; + public setDcsHandler(ident: number, handler: IDcsHandler): void { + this._handlers[ident] = [handler]; } - public clearDcsHandler(collectAndFlag: string): void { - if (this._handlers[collectAndFlag]) delete this._handlers[collectAndFlag]; + public clearDcsHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; } public setDcsHandlerFallback(handler: DcsFallbackHandler): void { @@ -54,24 +54,24 @@ export class DcsParser implements IDcsParser { this.unhook(false); } this._active = []; - this._collectAndFlag = ''; + this._ident = 0; } - public hook(collect: string, params: IParams, flag: number): void { - this._collectAndFlag = collect + String.fromCharCode(flag); - this._active = this._handlers[this._collectAndFlag] || []; + public hook(ident: number, params: IParams): void { + this._ident = ident; + this._active = this._handlers[ident] || []; if (!this._active.length) { - this._handlerFb(this._collectAndFlag, 'HOOK', {collect, params, flag}); + this._handlerFb(this._ident, 'HOOK', params); } else { for (let j = this._active.length - 1; j >= 0; j--) { - this._active[j].hook(collect, params, flag); + this._active[j].hook(params); } } } public put(data: Uint32Array, start: number, end: number): void { if (!this._active.length) { - this._handlerFb(this._collectAndFlag, 'PUT', utf32ToString(data, start, end)); + this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end)); } else { for (let j = this._active.length - 1; j >= 0; j--) { this._active[j].put(data, start, end); @@ -81,7 +81,7 @@ export class DcsParser implements IDcsParser { public unhook(success: boolean): void { if (!this._active.length) { - this._handlerFb(this._collectAndFlag, 'UNHOOK', success); + this._handlerFb(this._ident, 'UNHOOK', success); } else { let j = this._active.length - 1; for (; j >= 0; j--) { @@ -96,7 +96,7 @@ export class DcsParser implements IDcsParser { } } this._active = []; - this._collectAndFlag = ''; + this._ident = 0; } } @@ -109,9 +109,9 @@ export class DcsHandlerFactory implements IDcsHandler { private _params: IParams | undefined; private _hitLimit: boolean = false; - constructor(private _handler: (params: IParams, data: string) => any) {} + constructor(private _handler: (data: string, params: IParams) => any) {} - public hook(collect: string, params: IParams, flag: number): void { + public hook(params: IParams): void { this._params = params.clone(); this._data = ''; this._hitLimit = false; @@ -133,7 +133,7 @@ export class DcsHandlerFactory implements IDcsHandler { if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._params ? this._params : new Params(), this._data); + ret = this._handler(this._data, this._params ? this._params : new Params()); } this._params = undefined; this._data = ''; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 6b14e41b..a5bcbfb7 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -73,10 +73,14 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { return this._params; } public get collect(): string { - return this._collect; + return this.identToString(this._collect); } public set collect(value: string) { - this._collect = value; + this._collect = 0; + for (let i = 0; i < value.length; ++i) { + this._collect <<= 8; + this._collect |= value.charCodeAt(i); + } } public mockOscParser(): void { this._oscParser = oscPutParser; @@ -111,8 +115,8 @@ const testTerminal: any = { actionESC: function (collect: string, flag: string): void { this.calls.push(['esc', collect, flag]); }, - actionDCSHook: function (collect: string, params: IParams, flag: string): void { - this.calls.push(['dcs hook', collect, params.toArray(), flag]); + actionDCSHook: function (params: IParams): void { + this.calls.push(['dcs hook', params.toArray()]); }, actionDCSPrint: function (s: string): void { this.calls.push(['dcs put', s]); @@ -144,11 +148,13 @@ let state: any; const testParser = new TestEscapeSequenceParser(); testParser.mockOscParser(); testParser.setPrintHandler(testTerminal.print.bind(testTerminal)); -testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +testParser.setCsiHandlerFallback((ident: number, params: IParams) => { + const id = testParser.identToString(ident); + testTerminal.actionCSI(id.slice(0, -1), params, id.slice(-1)); }); -testParser.setEscHandlerFallback((collect: string, flag: number) => { - testTerminal.actionESC(collect, String.fromCharCode(flag)); +testParser.setEscHandlerFallback((ident: number) => { + const id = testParser.identToString(ident); + testTerminal.actionESC(id.slice(0, -1), id.slice(-1)); }); testParser.setExecuteHandlerFallback((code: number) => { testTerminal.actionExecute(String.fromCharCode(code)); @@ -160,7 +166,7 @@ testParser.setOscHandlerFallback((identifier, action, data) => { testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => { switch (action) { case 'HOOK': - testTerminal.actionDCSHook(payload.collect, payload.params, String.fromCharCode(payload.flag)); + testTerminal.actionDCSHook(payload); break; case 'PUT': testTerminal.actionDCSPrint(payload); @@ -939,7 +945,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_ENTRY; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -952,7 +958,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_PARAM; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -965,7 +971,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -1024,14 +1030,14 @@ describe('EscapeSequenceParser', function (): void { }); it('single DCS', function (): void { test('\x1bP1;2;3+$aäbc;däe\x9c', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'äbc;däe'], ['dcs unhook'] ], null); }); it('multi DCS', function (): void { test('\x1bP1;2;3+$abc;de', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'] ], null); testTerminal.clear(); @@ -1043,7 +1049,7 @@ describe('EscapeSequenceParser', function (): void { it('print + DCS(C1)', function (): void { test('abc\x901;2;3+$abc;de\x9c', [ ['print', 'abc'], - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'], ['dcs unhook'] ], null); @@ -1086,7 +1092,7 @@ describe('EscapeSequenceParser', function (): void { 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 hook', [1, 2, [-1, 55], 3]], ['dcs put', 'bc;de'], ['dcs unhook'] ], null); @@ -1120,7 +1126,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_PASSTHROUGH; parse(parser, '\x901;2;3+$a€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); + testTerminal.compare([['dcs hook', [1, 2, 3]], ['dcs put', '€öäü']]); parser.reset(); testTerminal.clear(); }); @@ -1172,69 +1178,69 @@ describe('EscapeSequenceParser', function (): void { chai.expect(print).equal(''); }); it('ESC handler', function (): void { - parser2.setEscHandler('%G', function (): void { + parser2.setEscHandler({intermediates: '%', final: 'G'}, function (): void { esc.push('%G'); }); - parser2.setEscHandler('E', function (): void { + parser2.setEscHandler({final: 'E'}, function (): void { esc.push('E'); }); parse(parser2, INPUT); chai.expect(esc).eql(['%G', 'E']); - parser2.clearEscHandler('%G'); - parser2.clearEscHandler('%G'); // should not throw + parser2.clearEscHandler({intermediates: '%', final: 'G'}); + parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw clearAccu(); parse(parser2, INPUT); chai.expect(esc).eql(['E']); - parser2.clearEscHandler('E'); + parser2.clearEscHandler({final: 'E'}); clearAccu(); parse(parser2, INPUT); chai.expect(esc).eql([]); }); describe('ESC custom handlers', () => { it('prevent fallback', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G']); }); it('allow fallback', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return false; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G', 'default - %G']); }); it('Multiple custom handlers fallback once', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); - parser2.addEscHandler('%G', () => { esc.push('custom2 - %G'); return false; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom2 - %G', 'custom - %G']); }); it('Multiple custom handlers no fallback', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); - parser2.addEscHandler('%G', () => { esc.push('custom2 - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom2 - %G']); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setEscHandler('%G', () => order.push(1)); - parser2.addEscHandler('%G', () => { order.push(2); return false; }); - parser2.addEscHandler('%G', () => { order.push(3); return false; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => order.push(1)); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; }); parse(parser2, '\x1b%G'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - const dispo = parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); parse(parser2, INPUT); chai.expect(esc).eql(['default - %G']); }); it('Should not corrupt the parser when dispose is called twice', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - const dispo = parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); dispo.dispose(); parse(parser2, INPUT); @@ -1242,13 +1248,13 @@ describe('EscapeSequenceParser', function (): void { }); }); it('CSI handler', function (): void { - parser2.setCsiHandler('m', function (params: IParams, collect: string): void { - csi.push(['m', params.toArray(), collect]); + parser2.setCsiHandler({final: 'm'}, function (params: IParams): void { + csi.push(['m', params.toArray(), '']); }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - parser2.clearCsiHandler('m'); - parser2.clearCsiHandler('m'); // should not throw + parser2.clearCsiHandler({final: 'm'}); + parser2.clearCsiHandler({final: 'm'}); // should not throw clearAccu(); parse(parser2, INPUT); chai.expect(csi).eql([]); @@ -1256,16 +1262,16 @@ describe('EscapeSequenceParser', function (): void { describe('CSI custom handlers', () => { it('Prevent fallback', () => { 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; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); 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, 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; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); 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], '']]); @@ -1273,9 +1279,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { 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; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1284,9 +1290,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { 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; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([], 'Should not fallback once'); @@ -1294,16 +1300,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setCsiHandler('m', () => order.push(1)); - parser2.addCsiHandler('m', () => { order.push(2); return false; }); - parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parser2.setCsiHandler({final: 'm'}, () => order.push(1)); + parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; }); + parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { 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; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1311,8 +1317,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { 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; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1415,9 +1421,9 @@ describe('EscapeSequenceParser', function (): void { }); }); it('DCS handler', function (): void { - parser2.setDcsHandler('+p', { - hook: function (collect: string, params: IParams, flag: number): void { - dcs.push(['hook', collect, params.toArray(), flag]); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, { + hook: function (params: IParams): void { + dcs.push(['hook', '', params.toArray(), 0]); }, put: function (data: Uint32Array, start: number, end: number): void { let s = ''; @@ -1433,12 +1439,12 @@ describe('EscapeSequenceParser', function (): void { parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); chai.expect(dcs).eql([ - ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], + ['hook', '', [1, 2, 3], 0], ['put', 'abc'], ['put', ';de'], ['unhook'] ]); - parser2.clearDcsHandler('+p'); - parser2.clearDcsHandler('+p'); // should not throw + parser2.clearDcsHandler({intermediates: '+', final: 'p'}); + parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw clearAccu(); parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); @@ -1448,54 +1454,54 @@ describe('EscapeSequenceParser', function (): void { const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; it('Prevent fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]); }); it('Allow fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers fallback once', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers no fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory(() => order.push(1))); - parser2.addDcsHandler('+p', new DcsHandlerFactory(() => { order.push(2); return false; })); - parser2.addDcsHandler('+p', new DcsHandlerFactory(() => { order.push(3); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => order.push(1))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(2); return false; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(3); return false; })); parse(parser2, DCS_INPUT); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); }); it('Should not corrupt the parser when dispose is called twice', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); dispo.dispose(); parse(parser2, DCS_INPUT); @@ -1513,8 +1519,7 @@ describe('EscapeSequenceParser', function (): void { position: 6, code: '€'.charCodeAt(0), currentState: ParserState.CSI_PARAM, - osc: '', - collect: '', + collect: 0, params: Params.fromArray([1, 2, 0]), // extra zero here abort: false }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 4d30a549..8342de4c 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler, IFunctionIdentifier } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -206,17 +206,25 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { * 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 + * the optional `transitions` constructor argument and * reimplement the `parse` method. * * 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 + * This this is not in line with the latest ECMA-48 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. + * are set to -1 (no ZDM for sub parameters). + * + * About prefix and intermediate bytes: + * This parser follows the assumptions of the vt100.net parser with these restrictions: + * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f + * - max. two intermediates are respected, byte range 0x20 .. 0x2f + * Note that this is not in line with ECMA-48 which does not limit either of those. + * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently + * there are no known sequences that follow the broader definition of the specification. * * TODO: implement error recovery hook via error handler return values */ @@ -227,7 +235,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // buffers over several parse calls protected _params: Params; - protected _collect: string; + protected _collect: number; // handler lookup containers protected _printHandler: (data: Uint32Array, start: number, end: number) => void; @@ -241,8 +249,8 @@ 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: IParams, flag: number) => void; - protected _escHandlerFb: (collect: string, flag: number) => void; + protected _csiHandlerFb: (ident: number, params: IParams) => void; + protected _escHandlerFb: (ident: number) => void; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -252,14 +260,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.currentState = this.initialState; this._params = new Params(); // defaults to 32 storable params/subparams this._params.addParam(0); // ZDM - this._collect = ''; + this._collect = 0; 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: IParams, flag: number): void => { }; - this._escHandlerFb = (collect: string, flag: number): void => { }; + this._csiHandlerFb = (ident: number, params: IParams): void => { }; + this._escHandlerFb = (ident: number): void => { }; this._errorHandlerFb = (state: IParsingState): IParsingState => state; this._printHandler = this._printHandlerFb; this._executeHandlers = Object.create(null); @@ -270,7 +278,53 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) - this.setEscHandler('\\', () => {}); + this.setEscHandler({final: '\\'}, () => {}); + } + + private _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (finalRange[0] > finalCode || finalCode > finalRange[1]) { + throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`); + } + res <<= 8; + res |= finalCode; + + return res; + } + + public identToString(ident: number): string { + const res: string[] = []; + while (ident) { + res.push(String.fromCharCode(ident & 0xFF)); + ident >>= 8; + } + return res.reverse().join(''); } public dispose(): void { @@ -297,12 +351,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - addCsiHandler(flag: string, callback: CsiHandler): IDisposable { - const index = flag.charCodeAt(0); - if (this._csiHandlers[index] === undefined) { - this._csiHandlers[index] = []; + addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { + const ident = this._identifier(id); + if (this._csiHandlers[ident] === undefined) { + this._csiHandlers[ident] = []; } - const handlerList = this._csiHandlers[index]; + const handlerList = this._csiHandlers[ident]; handlerList.push(callback); return { dispose: () => { @@ -313,21 +367,22 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = [callback]; + setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { + this._csiHandlers[this._identifier(id)] = [callback]; } - clearCsiHandler(flag: string): void { - if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; + clearCsiHandler(id: IFunctionIdentifier): void { + if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; } - setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void { + setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { this._csiHandlerFb = callback; } - addEscHandler(collectAndFlag: string, callback: EscHandler): IDisposable { - if (this._escHandlers[collectAndFlag] === undefined) { - this._escHandlers[collectAndFlag] = []; + addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { + const ident = this._identifier(id, [0x30, 0x7e]); + if (this._escHandlers[ident] === undefined) { + this._escHandlers[ident] = []; } - const handlerList = this._escHandlers[collectAndFlag]; + const handlerList = this._escHandlers[ident]; handlerList.push(callback); return { dispose: () => { @@ -338,13 +393,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setEscHandler(collectAndFlag: string, callback: () => void): void { - this._escHandlers[collectAndFlag] = [callback]; + setEscHandler(id: IFunctionIdentifier, callback: () => void): void { + this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [callback]; } - clearEscHandler(collectAndFlag: string): void { - if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag]; + clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } - setEscHandlerFallback(callback: (collect: string, flag: number) => void): void { + setEscHandlerFallback(callback: (ident: number) => void): void { this._escHandlerFb = callback; } @@ -361,14 +416,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._oscParser.setOscHandlerFallback(handler); } - addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { - return this._dcsParser.addDcsHandler(collectAndFlag, handler); + addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.addDcsHandler(this._identifier(id), handler); } - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._dcsParser.setDcsHandler(collectAndFlag, handler); + setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { + this._dcsParser.setDcsHandler(this._identifier(id), handler); } - clearDcsHandler(collectAndFlag: string): void { - this._dcsParser.clearDcsHandler(collectAndFlag); + clearDcsHandler(id: IFunctionIdentifier): void { + this._dcsParser.clearDcsHandler(this._identifier(id)); } setDcsHandlerFallback(handler: DcsFallbackHandler): void { this._dcsParser.setDcsHandlerFallback(handler); @@ -387,11 +442,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM - this._collect = ''; + this._collect = 0; // this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } + + /** * Parse UTF32 codepoints in `data` up to `length`. * @@ -415,7 +472,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - // let dcsHandler: IDcsHandler = this._activeDcsHandler; let callback: Function | null = null; // process input string @@ -465,7 +521,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP position: i, code, currentState, - osc: '', // FIXME: what to send here? collect, params, abort: false @@ -475,16 +530,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.CSI_DISPATCH: // Trigger CSI Handler - const handlers = this._csiHandlers[code]; + const handlers = this._csiHandlers[collect << 8 | code]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { // undefined or true means success and to stop bubbling - if (handlers[j](params, collect) !== false) { + if (handlers[j](params) !== false) { break; } } if (j < 0) { - this._csiHandlerFb(collect, params, code); + this._csiHandlerFb(collect << 8 | code, params); } this.precedingCodepoint = 0; break; @@ -509,10 +564,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP i--; break; case ParserAction.COLLECT: - collect += String.fromCharCode(code); + collect |= code; break; case ParserAction.ESC_DISPATCH: - const handlersEsc = this._escHandlers[collect + String.fromCharCode(code)]; + const handlersEsc = this._escHandlers[collect << 8 | code]; let jj = handlersEsc ? handlersEsc.length - 1 : -1; for (; jj >= 0; jj--) { // undefined or true means success and to stop bubbling @@ -521,7 +576,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } if (jj < 0) { - this._escHandlerFb(collect, code); + this._escHandlerFb(collect << 8 | code); } this.precedingCodepoint = 0; break; @@ -529,10 +584,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc.reset(); params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; break; case ParserAction.DCS_HOOK: - dcs.hook(collect, params, code); + dcs.hook(collect << 8 | code, params); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f @@ -551,7 +606,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc.reset(); params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; this.precedingCodepoint = 0; break; case ParserAction.OSC_START: @@ -573,7 +628,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc.reset(); params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; this.precedingCodepoint = 0; break; } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 4ae7ebcc..b237cd10 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -53,10 +53,8 @@ export interface IParsingState { code: number; // current parser state currentState: ParserState; - // osc string buffer - osc: string; // collect buffer with intermediate characters - collect: string; + collect: number; // params buffer params: IParams; // should abort (default: false) @@ -67,7 +65,7 @@ export interface IHandlerCollection { [key: string]: T[]; } -export type CsiHandler = (params: IParams, collect: string) => boolean | void; +export type CsiHandler = (params: IParams) => boolean | void; export type EscHandler = () => boolean | void; /** @@ -93,12 +91,12 @@ export type EscHandler = () => boolean | void; * indicates whether the command was aborted. */ export interface IDcsHandler { - hook(collect: string, params: IParams, flag: number): void; + hook(params: IParams): void; put(data: Uint32Array, start: number, end: number): void; unhook(success: boolean): void | boolean; } -export type DcsFallbackHandler = (collectAndFlag: string, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; +export type DcsFallbackHandler = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; export interface IOscHandler { /** @@ -145,6 +143,11 @@ export interface IEscapeSequenceParser extends IDisposable { */ parse(data: Uint32Array, length: number): void; + /** + * Get string from ident number. + */ + identToString(ident: number): string; + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; clearPrintHandler(): void; @@ -152,25 +155,25 @@ export interface IEscapeSequenceParser extends IDisposable { clearExecuteHandler(flag: string): void; setExecuteHandlerFallback(callback: (code: number) => void): void; - setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void; - clearCsiHandler(flag: string): void; - setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void; - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; + setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void; + clearCsiHandler(id: IFunctionIdentifier): void; + setCsiHandlerFallback(callback: (identifier: number, params: IParams) => void): void; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - setEscHandler(collectAndFlag: string, callback: () => void): void; - clearEscHandler(collectAndFlag: string): void; - setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; - addEscHandler(collectAndFlag: string, handler: EscHandler): IDisposable; + setEscHandler(id: IFunctionIdentifier, callback: () => void): void; + clearEscHandler(id: IFunctionIdentifier): void; + setEscHandlerFallback(callback: (identifier: number) => void): void; + addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable; setOscHandler(ident: number, handler: IOscHandler): void; clearOscHandler(ident: number): void; setOscHandlerFallback(handler: OscFallbackHandler): void; addOscHandler(ident: number, handler: IOscHandler): IDisposable; - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; - clearDcsHandler(collectAndFlag: string): void; + setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; + clearDcsHandler(id: IFunctionIdentifier): void; setDcsHandlerFallback(handler: DcsFallbackHandler): void; - addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; + addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; setErrorHandler(callback: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; @@ -188,12 +191,18 @@ export interface IOscParser extends IDisposable { } export interface IDcsParser extends IDisposable { - addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; - clearDcsHandler(collectAndFlag: string): void; + addDcsHandler(ident: number, handler: IDcsHandler): IDisposable; + setDcsHandler(ident: number, handler: IDcsHandler): void; + clearDcsHandler(ident: number): void; setDcsHandlerFallback(handler: DcsFallbackHandler): void; reset(): void; - hook(collect: string, params: IParams, flag: number): void; + hook(ident: number, params: IParams): void; put(data: Uint32Array, start: number, end: number): void; unhook(success: boolean): void; } + +export interface IFunctionIdentifier { + prefix?: string; + intermediates?: string; + final: string; +} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index c9513f53..4ea65c10 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -11,7 +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'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -57,14 +57,14 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - 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 addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); } - public addDcsHandler(collectAndFlag: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable { - return this._core.addDcsHandler(collectAndFlag, (params: IParams, data: string) => callback(params.toArray(), data)); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); } - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._core.addEscHandler(collectAndFlag, handler); + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + return this._core.addEscHandler(id, handler); } 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 fe43617d..4b784642 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -340,7 +340,7 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate(` window.term.reset(); const _customCsiHandlerParams = []; - const _customCsiHandler = window.term.addCsiHandler('m', (params, collect) => { + const _customCsiHandler = window.term.addCsiHandler({final: 'm'}, (params, collect) => { _customCsiHandlerParams.push(params); return false; }, ''); @@ -356,15 +356,15 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate(` window.term.reset(); const _customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.addDcsHandler('+p', (params, data) => { + const _customDcsHandlerA = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { _customDcsHandlerCallStack.push(['A', params, data]); return false; }); - const _customDcsHandlerB = window.term.addDcsHandler('+p', (params, data) => { + const _customDcsHandlerB = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { _customDcsHandlerCallStack.push(['B', params, data]); return true; }); - const _customDcsHandlerC = window.term.addDcsHandler('+p', (params, data) => { + const _customDcsHandlerC = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { _customDcsHandlerCallStack.push(['C', params, data]); return false; }); @@ -380,15 +380,15 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate(` window.term.reset(); const _customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.addEscHandler('(B', () => { + const _customEscHandlerA = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { _customEscHandlerCallStack.push('A'); return false; }); - const _customEscHandlerB = window.term.addEscHandler('(B', () => { + const _customEscHandlerB = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { _customEscHandlerCallStack.push('B'); return true; }); - const _customEscHandlerC = window.term.addEscHandler('(B', () => { + const _customEscHandlerC = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { _customEscHandlerCallStack.push('C'); return false; }); diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index 19b135c7..a8b9fde9 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -19,7 +19,7 @@ function toUtf32(s: string): Uint32Array { } class DcsHandler implements IDcsHandler { - hook(collect: string, params: IParams, flag: number) : void {} + hook(params: IParams) : void {} put(data: Uint32Array, start: number, end: number) : void {} unhook() :void {} } @@ -32,42 +32,42 @@ perfContext('Parser throughput - 50MB data', () => { beforeEach(() => { parser = new EscapeSequenceParser(); parser.setPrintHandler((data, start, end) => {}); - parser.setCsiHandler('@', (params, collect) => {}); - parser.setCsiHandler('A', (params, collect) => {}); - parser.setCsiHandler('B', (params, collect) => {}); - parser.setCsiHandler('C', (params, collect) => {}); - parser.setCsiHandler('D', (params, collect) => {}); - parser.setCsiHandler('E', (params, collect) => {}); - parser.setCsiHandler('F', (params, collect) => {}); - parser.setCsiHandler('G', (params, collect) => {}); - parser.setCsiHandler('H', (params, collect) => {}); - parser.setCsiHandler('I', (params, collect) => {}); - parser.setCsiHandler('J', (params, collect) => {}); - parser.setCsiHandler('K', (params, collect) => {}); - parser.setCsiHandler('L', (params, collect) => {}); - parser.setCsiHandler('M', (params, collect) => {}); - parser.setCsiHandler('P', (params, collect) => {}); - parser.setCsiHandler('S', (params, collect) => {}); - parser.setCsiHandler('T', (params, collect) => {}); - parser.setCsiHandler('X', (params, collect) => {}); - parser.setCsiHandler('Z', (params, collect) => {}); - parser.setCsiHandler('`', (params, collect) => {}); - parser.setCsiHandler('a', (params, collect) => {}); - parser.setCsiHandler('b', (params, collect) => {}); - parser.setCsiHandler('c', (params, collect) => {}); - parser.setCsiHandler('d', (params, collect) => {}); - parser.setCsiHandler('e', (params, collect) => {}); - parser.setCsiHandler('f', (params, collect) => {}); - parser.setCsiHandler('g', (params, collect) => {}); - parser.setCsiHandler('h', (params, collect) => {}); - parser.setCsiHandler('l', (params, collect) => {}); - parser.setCsiHandler('m', (params, collect) => {}); - parser.setCsiHandler('n', (params, collect) => {}); - parser.setCsiHandler('p', (params, collect) => {}); - parser.setCsiHandler('q', (params, collect) => {}); - parser.setCsiHandler('r', (params, collect) => {}); - parser.setCsiHandler('s', (params, collect) => {}); - parser.setCsiHandler('u', (params, collect) => {}); + parser.setCsiHandler({final: '@'}, params => {}); + parser.setCsiHandler({final: 'A'}, params => {}); + parser.setCsiHandler({final: 'B'}, params => {}); + parser.setCsiHandler({final: 'C'}, params => {}); + parser.setCsiHandler({final: 'D'}, params => {}); + parser.setCsiHandler({final: 'E'}, params => {}); + parser.setCsiHandler({final: 'F'}, params => {}); + parser.setCsiHandler({final: 'G'}, params => {}); + parser.setCsiHandler({final: 'H'}, params => {}); + parser.setCsiHandler({final: 'I'}, params => {}); + parser.setCsiHandler({final: 'J'}, params => {}); + parser.setCsiHandler({final: 'K'}, params => {}); + parser.setCsiHandler({final: 'L'}, params => {}); + parser.setCsiHandler({final: 'M'}, params => {}); + parser.setCsiHandler({final: 'P'}, params => {}); + parser.setCsiHandler({final: 'S'}, params => {}); + parser.setCsiHandler({final: 'T'}, params => {}); + parser.setCsiHandler({final: 'X'}, params => {}); + parser.setCsiHandler({final: 'Z'}, params => {}); + parser.setCsiHandler({final: '`'}, params => {}); + parser.setCsiHandler({final: 'a'}, params => {}); + parser.setCsiHandler({final: 'b'}, params => {}); + parser.setCsiHandler({final: 'c'}, params => {}); + parser.setCsiHandler({final: 'd'}, params => {}); + parser.setCsiHandler({final: 'e'}, params => {}); + parser.setCsiHandler({final: 'f'}, params => {}); + parser.setCsiHandler({final: 'g'}, params => {}); + parser.setCsiHandler({final: 'h'}, params => {}); + parser.setCsiHandler({final: 'l'}, params => {}); + parser.setCsiHandler({final: 'm'}, params => {}); + parser.setCsiHandler({final: 'n'}, params => {}); + parser.setCsiHandler({final: 'p'}, params => {}); + parser.setCsiHandler({final: 'q'}, params => {}); + parser.setCsiHandler({final: 'r'}, params => {}); + parser.setCsiHandler({final: 's'}, params => {}); + parser.setCsiHandler({final: 'u'}, params => {}); parser.setExecuteHandler(C0.BEL, () => {}); parser.setExecuteHandler(C0.LF, () => {}); parser.setExecuteHandler(C0.VT, () => {}); @@ -82,23 +82,23 @@ perfContext('Parser throughput - 50MB data', () => { parser.setExecuteHandler(C1.HTS, () => {}); parser.setOscHandler(0, new OscHandlerFactory((data) => {})); parser.setOscHandler(2, new OscHandlerFactory((data) => {})); - parser.setEscHandler('7', () => {}); - parser.setEscHandler('8', () => {}); - parser.setEscHandler('D', () => {}); - parser.setEscHandler('E', () => {}); - parser.setEscHandler('H', () => {}); - parser.setEscHandler('M', () => {}); - parser.setEscHandler('=', () => {}); - parser.setEscHandler('>', () => {}); - parser.setEscHandler('c', () => {}); - parser.setEscHandler('n', () => {}); - parser.setEscHandler('o', () => {}); - parser.setEscHandler('|', () => {}); - parser.setEscHandler('}', () => {}); - parser.setEscHandler('~', () => {}); - parser.setEscHandler('%@', () => {}); - parser.setEscHandler('%G', () => {}); - parser.setDcsHandler('q', new DcsHandler()); + parser.setEscHandler({final: '7'}, () => {}); + parser.setEscHandler({final: '8'}, () => {}); + parser.setEscHandler({final: 'D'}, () => {}); + parser.setEscHandler({final: 'E'}, () => {}); + parser.setEscHandler({final: 'H'}, () => {}); + parser.setEscHandler({final: 'M'}, () => {}); + parser.setEscHandler({final: '='}, () => {}); + parser.setEscHandler({final: '>'}, () => {}); + parser.setEscHandler({final: 'c'}, () => {}); + parser.setEscHandler({final: 'n'}, () => {}); + parser.setEscHandler({final: 'o'}, () => {}); + parser.setEscHandler({final: '|'}, () => {}); + parser.setEscHandler({final: '}'}, () => {}); + parser.setEscHandler({final: '~'}, () => {}); + parser.setEscHandler({intermediates: '%', final: '@'}, () => {}); + parser.setEscHandler({intermediates: '%', final: 'G'}, () => {}); + parser.setDcsHandler({final: 'q'}, new DcsHandler()); }); perfContext('PRINT - a', () => { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 806074a2..417e03a9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -502,8 +502,8 @@ declare module 'xterm' { /** * Adds a handler for CSI escape sequences. - * @param flag The flag should be one-character string, which specifies the - * final character (e.g "m" for SGR) of the CSI sequence. + * @param id Specifies the function identifier under which the callback gets registered, + * e.g. {final: 'm'} for SGR. * @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). If the sequence has subparams the array will @@ -513,12 +513,12 @@ declare module 'xterm' { * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; + addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; /** * Adds a handler for DCS escape sequences. - * @param collect Should be a string, which specifies the collect and the - * final character (e.g "$q" for DECRQSS) of the DCS sequence. + * @param id Specifies the function identifier under which the callback gets registered, + * e.g. {intermediates: '$' final: 'q'} for DECRQSS. * @param callback The function to handle the escape sequence. Note that the * function will only be called once if the sequence finished sucessfully. * There is currently no way to intercept smaller data chunks, those will be stored up @@ -531,20 +531,19 @@ declare module 'xterm' { * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addDcsHandler(collect: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; /** * Adds a handler for ESC escape sequences. - * @param collect Should be a string, which specifies the collect and the - * final character (e.g "%G" for default charset selection) - * of the ESC sequence. + * @param id Specifies the function identifier under which the callback gets registered, + * e.g. {intermediates: '%' final: 'G'} for default charset selection. * @param callback The function to handle the escape sequence. * Return true if the sequence was handled; false if * we should try a previous handler (set by addEscHandler or setEscHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addEscHandler(collect: string, handler: () => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; /** * Adds a handler for OSC escape sequences. @@ -987,4 +986,22 @@ declare module 'xterm' { */ readonly width: number; } + + /** + * Data type to register a CSI, DCS or ESC callback in the parser. + */ + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e (\x30 .. \x7e for ESC). + */ + final: string; + } }