diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts new file mode 100644 index 0000000000..2d85c4665b --- /dev/null +++ b/src/EscapeSequenceParser.test.ts @@ -0,0 +1,1243 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ParserState, IDcsHandler, IParsingState } from './Types'; +import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from './EscapeSequenceParser'; +import * as chai from 'chai'; + +function r(a: number, b: number): string[] { + let c = b - a; + let arr = new Array(c); + while (c--) { + arr[c] = String.fromCharCode(--b); + } + return arr; +} + +// derived parser with access to internal states +class TestEscapeSequenceParser extends EscapeSequenceParser { + public get osc(): string { + return this._osc; + } + public set osc(value: string) { + this._osc = value; + } + public get params(): number[] { + return this._params; + } + public set params(value: number[]) { + this._params = value; + } + public get collect(): string { + return this._collect; + } + public set collect(value: string) { + this._collect = value; + } + public mockActiveDcsHandler(): void { + this._activeDcsHandler = this._dcsHandlerFb; + } +} + +// test object to collect parser actions and compare them with expected values +let testTerminal: any = { + calls: [], + clear: function (): void { + this.calls = []; + }, + compare: function (value: any): void { + chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here + }, + print: function (data: string, start: number, end: number): void { + this.calls.push(['print', data.substring(start, end)]); + }, + actionOSC: function (s: string): void { + this.calls.push(['osc', s]); + }, + actionExecute: function (flag: string): void { + this.calls.push(['exe', flag]); + }, + actionCSI: function (collect: string, params: number[], flag: string): void { + this.calls.push(['csi', collect, params, flag]); + }, + actionESC: function (collect: string, flag: string): void { + this.calls.push(['esc', collect, flag]); + }, + actionDCSHook: function (collect: string, params: number[], flag: string): void { + this.calls.push(['dcs hook', collect, params, flag]); + }, + actionDCSPrint: function (data: string, start: number, end: number): void { + this.calls.push(['dcs put', data.substring(start, end)]); + }, + actionDCSUnhook: function (): void { + this.calls.push(['dcs unhook']); + } +}; + +// dcs handler to map dcs actions into the test object `testTerminal` +class DcsTest implements IDcsHandler { + hook(collect: string, params: number[], flag: number): void { + testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); + } + put(data: string, start: number, end: number): void { + testTerminal.actionDCSPrint(data, start, end); + } + unhook(): void { + testTerminal.actionDCSUnhook(); + } +} + +let states: number[] = [ + ParserState.GROUND, + ParserState.ESCAPE, + ParserState.ESCAPE_INTERMEDIATE, + ParserState.CSI_ENTRY, + ParserState.CSI_PARAM, + ParserState.CSI_INTERMEDIATE, + ParserState.CSI_IGNORE, + ParserState.SOS_PM_APC_STRING, + ParserState.OSC_STRING, + ParserState.DCS_ENTRY, + ParserState.DCS_PARAM, + ParserState.DCS_IGNORE, + ParserState.DCS_INTERMEDIATE, + ParserState.DCS_PASSTHROUGH +]; +let state: any; + +// parser with Uint8Array based transition table +let parserUint = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE); +parserUint.setPrintHandler(testTerminal.print.bind(testTerminal)); +parserUint.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +}); +parserUint.setEscHandlerFallback((collect: string, flag: number) => { + testTerminal.actionESC(collect, String.fromCharCode(flag)); +}); +parserUint.setExecuteHandlerFallback((code: number) => { + testTerminal.actionExecute(String.fromCharCode(code)); +}); +parserUint.setOscHandlerFallback((identifier: number, data: string) => { + if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently + else testTerminal.actionOSC('' + identifier + ';' + data); +}); +parserUint.setDcsHandlerFallback(new DcsTest()); + +// array based transition table +let VT500_TRANSITION_TABLE_ARRAY = new TransitionTable(VT500_TRANSITION_TABLE.table.length); +VT500_TRANSITION_TABLE_ARRAY.table = new Array(VT500_TRANSITION_TABLE.table.length); +for (let i = 0; i < VT500_TRANSITION_TABLE.table.length; ++i) { + VT500_TRANSITION_TABLE_ARRAY.table[i] = VT500_TRANSITION_TABLE.table[i]; +} + +// parser with array based transition table +let parserArray = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE_ARRAY); +parserArray.setPrintHandler(testTerminal.print.bind(testTerminal)); +parserArray.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +}); +parserArray.setEscHandlerFallback((collect: string, flag: number) => { + testTerminal.actionESC(collect, String.fromCharCode(flag)); +}); +parserArray.setExecuteHandlerFallback((code: number) => { + testTerminal.actionExecute(String.fromCharCode(code)); +}); +parserArray.setOscHandlerFallback((identifier: number, data: string) => { + if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently + else testTerminal.actionOSC('' + identifier + ';' + data); +}); +parserArray.setDcsHandlerFallback(new DcsTest()); + +interface IRun { + tableType: string; + parser: TestEscapeSequenceParser; +} + + +describe('EscapeSequenceParser', function (): void { + let parser: TestEscapeSequenceParser | null = null; + const runs: IRun[] = [ + { tableType: 'Uint8Array', parser: parserUint }, + { tableType: 'Array', parser: parserArray } + ]; + runs.forEach(function (run: IRun): void { + describe('Parser init and methods / ' + run.tableType, function (): void { + before(function(): void { + parser = run.parser; + }); + it('constructor', function (): void { + let p: EscapeSequenceParser = new EscapeSequenceParser(); + chai.expect(p.transitions).equal(VT500_TRANSITION_TABLE); + p = new EscapeSequenceParser(VT500_TRANSITION_TABLE); + chai.expect(p.transitions).equal(VT500_TRANSITION_TABLE); + let tansitions: TransitionTable = new TransitionTable(10); + p = new EscapeSequenceParser(tansitions); + chai.expect(p.transitions).equal(tansitions); + }); + it('inital states', function (): void { + chai.expect(parser.initialState).equal(ParserState.GROUND); + chai.expect(parser.currentState).equal(ParserState.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + }); + it('reset states', function (): void { + parser.currentState = 124; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + + parser.reset(); + chai.expect(parser.currentState).equal(ParserState.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + }); + }); + }); + runs.forEach(function (run: IRun): void { + describe('state transitions and actions / ' + run.tableType, function (): void { + before(function(): void { + parser = run.parser; + }); + it('state GROUND execute action', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.GROUND; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state GROUND print action', function (): void { + parser.reset(); + testTerminal.clear(); + let printables = r(0x20, 0x7f); // NOTE: DEL excluded + for (let i = 0; i < printables.length; ++i) { + parser.currentState = ParserState.GROUND; + parser.parse(printables[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['print', printables[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> GROUND with actions', function (): void { + let exes = [ + '\x18', '\x1a', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', + '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' + ]; + let exceptions = { + 8: { '\x18': [], '\x1a': [] } // simply abort osc state + }; + parser.reset(); + testTerminal.clear(); + for (state in states) { + for (let i = 0; i < exes.length; ++i) { + parser.currentState = state; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare(((exceptions[state]) ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + parser.parse('\x9c'); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> ESCAPE with clear', function (): void { + parser.reset(); + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [23]; + parser.collect = '#'; + parser.parse('\x1b'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + } + }); + it('state ESCAPE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.ESCAPE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let dispatches = r(0x30, 0x50); + dispatches.concat(r(0x51, 0x58)); + dispatches.concat(['\x59', '\x5a', '\x5c']); + dispatches.concat(r(0x60, 0x7f)); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['esc', '', dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('state ESCAPE_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x30, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['esc', '', collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + parser.reset(); + // C0 + parser.currentState = ParserState.ESCAPE; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + parser.parse('['); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + parser.parse('\x9b'); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + } + }); + it('state CSI_ENTRY execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_ENTRY ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_ENTRY; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.CSI_ENTRY; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_PARAM execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.CSI_PARAM; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('state CSI_PARAM ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_PARAM; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_INTERMEDIATE collect', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + parser.reset(); + parser.currentState = ParserState.CSI_ENTRY; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + parser.reset(); + }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0]); + parser.reset(); + } + }); + it('state CSI_IGNORE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_IGNORE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + let ignored = r(0x20, 0x40); + ignored.concat(['\x7f']); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_IGNORE --> GROUND', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + parser.reset(); + // C0 + let initializers = ['\x58', '\x5e', '\x5f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse('\x1b' + initializers[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + // C1 + for (state in states) { + parser.currentState = state; + initializers = ['\x98', '\x9e', '\x9f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse(initializers[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + } + }); + it('state SOS_PM_APC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = r(0x00, 0x18); + ignored.concat(['\x19']); + ignored.concat(r(0x1c, 0x20)); + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.SOS_PM_APC_STRING; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + }); + it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + parser.reset(); + // C0 + parser.parse('\x1b]'); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.parse('\x9d'); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + parser.reset(); + } + }); + it('state OSC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.OSC_STRING; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + chai.expect(parser.osc).equal(''); + parser.reset(); + } + }); + it('state OSC_STRING put action', function (): void { + parser.reset(); + let puts = r(0x20, 0x80); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = ParserState.OSC_STRING; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + chai.expect(parser.osc).equal(puts[i]); + parser.reset(); + } + }); + it('state DCS_ENTRY', function (): void { + parser.reset(); + // C0 + parser.parse('\x1bP'); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.parse('\x90'); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.DCS_ENTRY; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_PARAM ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + parser.reset(); + } + }); + it('state DCS_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.DCS_PARAM; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + parser.reset(); + parser.currentState = ParserState.DCS_ENTRY; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + }); + it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + } + }); + it('state DCS_IGNORE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_IGNORE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse('\x20' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.collect).equal('\x20'); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH put action', function (): void { + parser.reset(); + testTerminal.clear(); + let puts = r(0x00, 0x18); + puts.concat(['\x19']); + puts.concat(r(0x1c, 0x20)); + puts.concat(r(0x20, 0x7f)); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.mockActiveDcsHandler(); + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs put', puts[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + }); + }); + + runs.forEach(function (run: IRun): void { + let test: Function | null = null; + describe('escape sequence examples / ' + run.tableType, function (): void { + before(function(): void { + parser = run.parser; + test = function(s: string, value: any, noReset: any): void { + if (!noReset) { + parser.reset(); + testTerminal.clear(); + } + parser.parse(s); + testTerminal.compare(value); + }; + }); + it('CSI with print and execute', function (): void { + test('\x1b[<31;5mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('OSC', function (): void { + test('\x1b]0;abc123€öäü\x07', [ + ['osc', '0;abc123€öäü'] + ], null); + }); + it('single DCS', function (): void { + test('\x1bP1;2;3+$abc;de\x9c', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('multi DCS', function (): void { + test('\x1bP1;2;3+$abc;de', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'] + ], null); + testTerminal.clear(); + test('abc\x9c', [ + ['dcs put', 'abc'], + ['dcs unhook'] + ], true); + }); + it('print + DCS(C1)', function (): void { + test('abc\x901;2;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('print + PM(C1) + print', function (): void { + test('abc\x98123tzf\x9cdefg', [ + ['print', 'abc'], + ['print', 'defg'] + ], null); + }); + it('print + OSC(C1) + print', function (): void { + test('abc\x9d123tzf\x9cdefg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + it('error recovery', function (): void { + test('\x1b[1€abcdefg\x9b<;c', [ + ['print', 'abcdefg'], + ['csi', '<', [0, 0], 'c'] + ], null); + }); + }); + }); + + describe('coverage tests', function (): void { + it('CSI_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_IGNORE; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_IGNORE; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_PASSTHROUGH error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.parse('\x901;2;3+$a€öäü'); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); + parser.reset(); + testTerminal.clear(); + }); + it('error else of if (code > 159)', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.GROUND; + parser.parse('\x1e'); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + }); + + describe('set/clear handler', function (): void { + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; + let parser2 = null; + let print = ''; + let esc = []; + let csi = []; + let exe = []; + let osc = []; + let dcs = []; + function clearAccu(): void { + print = ''; + esc = []; + csi = []; + exe = []; + osc = []; + dcs = []; + } + beforeEach(function (): void { + parser2 = new TestEscapeSequenceParser(); + clearAccu(); + }); + it('print handler', function (): void { + parser2.setPrintHandler(function (data: string, start: number, end: number): void { + print += data.substring(start, end); + }); + parser2.parse(INPUT); + chai.expect(print).equal('hello world!$>'); + parser2.clearPrintHandler(); + parser2.clearPrintHandler(); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(print).equal(''); + }); + it('ESC handler', function (): void { + parser2.setEscHandler('%G', function (): void { + esc.push('%G'); + }); + parser2.setEscHandler('E', function (): void { + esc.push('E'); + }); + parser2.parse(INPUT); + chai.expect(esc).eql(['%G', 'E']); + parser2.clearEscHandler('%G'); + parser2.clearEscHandler('%G'); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(esc).eql(['E']); + parser2.clearEscHandler('E'); + clearAccu(); + parser2.parse(INPUT); + chai.expect(esc).eql([]); + }); + it('CSI handler', function (): void { + parser2.setCsiHandler('m', function (params: number[], collect: string): void { + csi.push(['m', params, collect]); + }); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + parser2.clearCsiHandler('m'); + parser2.clearCsiHandler('m'); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(csi).eql([]); + }); + it('EXECUTE handler', function (): void { + parser2.setExecuteHandler('\n', function (): void { + exe.push('\n'); + }); + parser2.setExecuteHandler('\r', function (): void { + exe.push('\r'); + }); + parser2.parse(INPUT); + chai.expect(exe).eql(['\r', '\n']); + parser2.clearExecuteHandler('\r'); + parser2.clearExecuteHandler('\r'); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(exe).eql(['\n']); + }); + it('OSC handler', function (): void { + parser2.setOscHandler(1, function (data: string): void { + osc.push([1, data]); + }); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + parser2.clearOscHandler(1); + parser2.clearOscHandler(1); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(osc).eql([]); + }); + it('DCS handler', function (): void { + parser2.setDcsHandler('+p', { + hook: function (collect: string, params: number[], flag: number): void { + dcs.push(['hook', collect, params, flag]); + }, + put: function (data: string, start: number, end: number): void { + dcs.push(['put', data.substring(start, end)]); + }, + unhook: function (): void { + dcs.push(['unhook']); + } + }); + parser2.parse('\x1bP1;2;3+pabc'); + parser2.parse(';de\x9c'); + chai.expect(dcs).eql([ + ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], + ['put', 'abc'], ['put', ';de'], + ['unhook'] + ]); + parser2.clearDcsHandler('+p'); + parser2.clearDcsHandler('+p'); // should not throw + clearAccu(); + parser2.parse('\x1bP1;2;3+pabc'); + parser2.parse(';de\x9c'); + chai.expect(dcs).eql([]); + }); + it('ERROR handler', function (): void { + let errorState: IParsingState = null; + parser2.setErrorHandler(function (state: IParsingState): IParsingState { + errorState = state; + return state; + }); + parser2.parse('\x1b[1;2;€;3m'); // faulty escape sequence + chai.expect(errorState).eql({ + position: 6, + code: '€'.charCodeAt(0), + currentState: ParserState.CSI_PARAM, + print: -1, + dcs: -1, + osc: '', + collect: '', + params: [1, 2, 0], // extra zero here + abort: false + }); + parser2.clearErrorHandler(); + parser2.clearErrorHandler(); // should not throw + errorState = null; + parser2.parse('\x1b[1;2;a;3m'); + chai.expect(errorState).eql(null); + }); + }); + // TODO: error conditions and error recovery (not implemented yet in parser) +}); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts new file mode 100644 index 0000000000..dea87f8d56 --- /dev/null +++ b/src/EscapeSequenceParser.ts @@ -0,0 +1,544 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; + +/** + * Returns an array filled with numbers between the low and high parameters (right exclusive). + * @param low The low number. + * @param high The high number. + */ +function r(low: number, high: number): number[] { + let c = high - low; + let arr = new Array(c); + while (c--) { + arr[c] = --high; + } + return arr; +} + +/** + * Transition table for EscapeSequenceParser. + * NOTE: data in the underlying table is packed like this: + * currentState << 8 | characterCode --> action << 4 | nextState + */ +export class TransitionTable { + public table: Uint8Array | number[]; + + constructor(length: number) { + this.table = (typeof Uint8Array === 'undefined') + ? new Array(length) + : new Uint8Array(length); + } + + /** + * Add a transition to the transition table. + * @param code input character code + * @param state current parser state + * @param action parser action to be done + * @param next next parser state + */ + add(code: number, state: number, action: number | null, next: number | null): void { + this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); + } + + /** + * Add transitions for multiple input character codes. + * @param codes input character code array + * @param state current parser state + * @param action parser action to be done + * @param next next parser state + */ + addMany(codes: number[], state: number, action: number | null, next: number | null): void { + for (let i = 0; i < codes.length; i++) { + this.add(codes[i], state, action, next); + } + } +} + + +/** + * Default definitions for the VT500_TRANSITION_TABLE. + */ +let PRINTABLES = r(0x20, 0x7f); +let EXECUTABLES = r(0x00, 0x18); +EXECUTABLES.push(0x19); +EXECUTABLES.concat(r(0x1c, 0x20)); +const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND; + +/** + * VT500 compatible transition table. + * Taken from https://vt100.net/emu/dec_ansi_parser. + */ +export const VT500_TRANSITION_TABLE = (function (): TransitionTable { + let table: TransitionTable = new TransitionTable(4095); + + let states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1); + let state: any; + + // table with default transition [any] --> DEFAULT_TRANSITION + for (state in states) { + // NOTE: table lookup is capped at 0xa0 in parse to keep the table small + for (let code = 0; code < 160; ++code) { + table.add(code, state, ParserAction.ERROR, ParserState.GROUND); + } + } + // printables + table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); + // global anywhere rules + for (state in states) { + table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND); + table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator + table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC + table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC + table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI + table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS + } + // rules for executables and 7f + table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE); + table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE); + table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); + table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY); + table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY); + table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM); + table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM); + table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE); + table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE); + table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE); + table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE); + table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE); + // osc + table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING); + table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND); + table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); + // sos/pm/apc does nothing + table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); + // csi entries + table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); + table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); + table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM); + table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); + table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + // esc_intermediate + table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE); + table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + // dcs entry + table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY); + table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); + table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM); + table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); + table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); + table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + return table; +})(); + +/** + * Dummy DCS handler as default fallback. + */ +class DcsDummy implements IDcsHandler { + hook(collect: string, params: number[], flag: number): void { } + put(data: string, start: number, end: number): void { } + unhook(): void { } +} + +/** + * EscapeSequenceParser. + * This class implements the ANSI/DEC compatible parser described by + * Paul Williams (https://vt100.net/emu/dec_ansi_parser). + * To implement custom ANSI compliant escape sequences it is not needed to + * alter this parser, instead consider registering a custom handler. + * For non ANSI compliant sequences change the transition table with + * the optional `transitions` contructor argument and + * reimplement the `parse` method. + * NOTE: The parameter element notation is currently not supported. + * TODO: implement error recovery hook via error handler return values + */ +export class EscapeSequenceParser implements IEscapeSequenceParser { + public initialState: number; + public currentState: number; + + // buffers over several parse calls + protected _osc: string; + protected _params: number[]; + protected _collect: string; + + // handler lookup containers + protected _printHandler: (data: string, start: number, end: number) => void; + protected _executeHandlers: any; + protected _csiHandlers: any; + protected _escHandlers: any; + protected _oscHandlers: any; + protected _dcsHandlers: any; + protected _activeDcsHandler: IDcsHandler | null; + protected _errorHandler: (state: IParsingState) => IParsingState; + + // fallback handlers + protected _printHandlerFb: (data: string, start: number, end: number) => void; + protected _executeHandlerFb: (code: number) => void; + protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; + protected _escHandlerFb: (collect: string, flag: number) => void; + protected _oscHandlerFb: (identifier: number, data: string) => void; + protected _dcsHandlerFb: IDcsHandler; + protected _errorHandlerFb: (state: IParsingState) => IParsingState; + + constructor(readonly transitions: TransitionTable = VT500_TRANSITION_TABLE) { + this.initialState = ParserState.GROUND; + this.currentState = this.initialState; + this._osc = ''; + this._params = [0]; + this._collect = ''; + + // set default fallback handlers and handler lookup containers + this._printHandlerFb = (data, start, end): void => { }; + this._executeHandlerFb = (code: number): void => { }; + this._csiHandlerFb = (collect: string, params: number[], flag: number): void => { }; + this._escHandlerFb = (collect: string, flag: number): void => { }; + this._oscHandlerFb = (identifier: number, data: string): void => { }; + this._dcsHandlerFb = new DcsDummy(); + this._errorHandlerFb = (state: IParsingState): IParsingState => state; + this._printHandler = this._printHandlerFb; + this._executeHandlers = Object.create(null); + this._csiHandlers = Object.create(null); + this._escHandlers = Object.create(null); + this._oscHandlers = Object.create(null); + this._dcsHandlers = Object.create(null); + this._activeDcsHandler = null; + this._errorHandler = this._errorHandlerFb; + } + + setPrintHandler(callback: (data: string, start: number, end: number) => void): void { + this._printHandler = callback; + } + clearPrintHandler(): void { + this._printHandler = this._printHandlerFb; + } + + setExecuteHandler(flag: string, callback: () => void): void { + this._executeHandlers[flag.charCodeAt(0)] = callback; + } + clearExecuteHandler(flag: string): void { + if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; + } + setExecuteHandlerFallback(callback: (code: number) => void): void { + this._executeHandlerFb = callback; + } + + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { + this._csiHandlers[flag.charCodeAt(0)] = callback; + } + clearCsiHandler(flag: string): void { + if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; + } + setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void { + this._csiHandlerFb = callback; + } + + setEscHandler(collectAndFlag: string, callback: () => void): void { + this._escHandlers[collectAndFlag] = callback; + } + clearEscHandler(collectAndFlag: string): void { + if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag]; + } + setEscHandlerFallback(callback: (collect: string, flag: number) => void): void { + this._escHandlerFb = callback; + } + + setOscHandler(ident: number, callback: (data: string) => void): void { + this._oscHandlers[ident] = callback; + } + clearOscHandler(ident: number): void { + if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; + } + setOscHandlerFallback(callback: (identifier: number, data: string) => void): void { + this._oscHandlerFb = callback; + } + + setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { + this._dcsHandlers[collectAndFlag] = handler; + } + clearDcsHandler(collectAndFlag: string): void { + if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag]; + } + setDcsHandlerFallback(handler: IDcsHandler): void { + this._dcsHandlerFb = handler; + } + + setErrorHandler(callback: (state: IParsingState) => IParsingState): void { + this._errorHandler = callback; + } + clearErrorHandler(): void { + this._errorHandler = this._errorHandlerFb; + } + + reset(): void { + this.currentState = this.initialState; + this._osc = ''; + this._params = [0]; + this._collect = ''; + this._activeDcsHandler = null; + } + + parse(data: string): void { + let code = 0; + let transition = 0; + let error = false; + let currentState = this.currentState; + let print = -1; + let dcs = -1; + let osc = this._osc; + let collect = this._collect; + let params = this._params; + const table: Uint8Array | number[] = this.transitions.table; + let dcsHandler: IDcsHandler | null = this._activeDcsHandler; + let callback: Function | null = null; + + // process input string + const l = data.length; + for (let i = 0; i < l; ++i) { + code = data.charCodeAt(i); + + // shortcut for most chars (print action) + if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) { + print = (~print) ? print : i; + do code = data.charCodeAt(++i); + while (i < l && code > 0x1f && code < 0x80); + i--; + continue; + } + + // shortcut for CSI params + if (currentState === ParserState.CSI_PARAM && (code > 0x2f && code < 0x39)) { + params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + continue; + } + + // normal transition & action lookup + transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; + switch (transition >> 4) { + case ParserAction.PRINT: + print = (~print) ? print : i; + break; + case ParserAction.EXECUTE: + if (~print) { + this._printHandler(data, print, i); + print = -1; + } + callback = this._executeHandlers[code]; + if (callback) callback(); + else this._executeHandlerFb(code); + break; + case ParserAction.IGNORE: + // handle leftover print or dcs chars + if (~print) { + this._printHandler(data, print, i); + print = -1; + } else if (~dcs) { + dcsHandler.put(data, dcs, i); + dcs = -1; + } + break; + case ParserAction.ERROR: + // chars higher than 0x9f are handled by this action + // to keep the transition table small + if (code > 0x9f) { + switch (currentState) { + case ParserState.GROUND: + print = (~print) ? print : i; + break; + case ParserState.OSC_STRING: + osc += String.fromCharCode(code); + transition |= ParserState.OSC_STRING; + break; + case ParserState.CSI_IGNORE: + transition |= ParserState.CSI_IGNORE; + break; + case ParserState.DCS_IGNORE: + transition |= ParserState.DCS_IGNORE; + break; + case ParserState.DCS_PASSTHROUGH: + dcs = (~dcs) ? dcs : i; + transition |= ParserState.DCS_PASSTHROUGH; + break; + default: + error = true; + } + } else { + error = true; + } + // if we end up here a real error happened + if (error) { + let inject: IParsingState = this._errorHandler( + { + position: i, + code, + currentState, + print, + dcs, + osc, + collect, + params, + abort: false + }); + if (inject.abort) return; + // TODO: inject return values + error = false; + } + break; + case ParserAction.CSI_DISPATCH: + callback = this._csiHandlers[code]; + if (callback) callback(params, collect); + else this._csiHandlerFb(collect, params, code); + break; + case ParserAction.PARAM: + if (code === 0x3b) params.push(0); + else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + break; + case ParserAction.COLLECT: + collect += String.fromCharCode(code); + break; + case ParserAction.ESC_DISPATCH: + callback = this._escHandlers[collect + String.fromCharCode(code)]; + if (callback) callback(collect, code); + else this._escHandlerFb(collect, code); + break; + case ParserAction.CLEAR: + if (~print) { + this._printHandler(data, print, i); + print = -1; + } + osc = ''; + params = [0]; + collect = ''; + dcs = -1; + break; + case ParserAction.DCS_HOOK: + dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)]; + if (!dcsHandler) dcsHandler = this._dcsHandlerFb; + dcsHandler.hook(collect, params, code); + break; + case ParserAction.DCS_PUT: + dcs = (~dcs) ? dcs : i; + break; + case ParserAction.DCS_UNHOOK: + if (dcsHandler) { + if (~dcs) dcsHandler.put(data, dcs, i); + dcsHandler.unhook(); + dcsHandler = null; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + osc = ''; + params = [0]; + collect = ''; + dcs = -1; + break; + case ParserAction.OSC_START: + if (~print) { + this._printHandler(data, print, i); + print = -1; + } + osc = ''; + break; + case ParserAction.OSC_PUT: + osc += data.charAt(i); + break; + case ParserAction.OSC_END: + if (osc && code !== 0x18 && code !== 0x1a) { + // NOTE: OSC subparsing is not part of the original parser + // we do basic identifier parsing here to offer a jump table for OSC as well + let idx = osc.indexOf(';'); + if (idx === -1) { + this._oscHandlerFb(-1, osc); // this is an error (malformed OSC) + } else { + // Note: NaN is not handled here + // either catch it with the fallback handler + // or with an explicit NaN OSC handler + let identifier = parseInt(osc.substring(0, idx)); + let content = osc.substring(idx + 1); + callback = this._oscHandlers[identifier]; + if (callback) callback(content); + else this._oscHandlerFb(identifier, content); + } + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + osc = ''; + params = [0]; + collect = ''; + dcs = -1; + break; + } + currentState = transition & 15; + } + + // push leftover pushable buffers to terminal + if (currentState === ParserState.GROUND && ~print) { + this._printHandler(data, print, data.length); + } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { + dcsHandler.put(data, dcs, data.length); + } + + // save non pushable buffers + this._osc = osc; + this._collect = collect; + this._params = params; + + // save active dcs handler reference + this._activeDcsHandler = dcsHandler; + + // save state + this.currentState = currentState; + } +} diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts index 11cbd84eb6..e35f01dd07 100644 --- a/src/EscapeSequences.ts +++ b/src/EscapeSequences.ts @@ -77,3 +77,74 @@ export namespace C0 { /** Delete (Caret = ^?) */ export const DEL = '\x7f'; } + +/** + * C1 control codes + * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes + */ +export namespace C1 { + /** padding character */ + export const PAD = '\x80'; + /** High Octet Preset */ + export const HOP = '\x81'; + /** Break Permitted Here */ + export const BPH = '\x82'; + /** No Break Here */ + export const NBH = '\x83'; + /** Index */ + export const IND = '\x84'; + /** Next Line */ + export const NEL = '\x85'; + /** Start of Selected Area */ + export const SSA = '\x86'; + /** End of Selected Area */ + export const ESA = '\x87'; + /** Horizontal Tabulation Set */ + export const HTS = '\x88'; + /** Horizontal Tabulation With Justification */ + export const HTJ = '\x89'; + /** Vertical Tabulation Set */ + export const VTS = '\x8a'; + /** Partial Line Down */ + export const PLD = '\x8b'; + /** Partial Line Up */ + export const PLU = '\x8c'; + /** Reverse Index */ + export const RI = '\x8d'; + /** Single-Shift 2 */ + export const SS2 = '\x8e'; + /** Single-Shift 3 */ + export const SS3 = '\x8f'; + /** Device Control String */ + export const DCS = '\x90'; + /** Private Use 1 */ + export const PU1 = '\x91'; + /** Private Use 2 */ + export const PU2 = '\x92'; + /** Set Transmit State */ + export const STS = '\x93'; + /** Destructive backspace, intended to eliminate ambiguity about meaning of BS. */ + export const CCH = '\x94'; + /** Message Waiting */ + export const MW = '\x95'; + /** Start of Protected Area */ + export const SPA = '\x96'; + /** End of Protected Area */ + export const EPA = '\x97'; + /** Start of String */ + export const SOS = '\x98'; + /** Single Graphic Character Introducer */ + export const SGCI = '\x99'; + /** Single Character Introducer */ + export const SCI = '\x9a'; + /** Control Sequence Introducer */ + export const CSI = '\x9b'; + /** String Terminator */ + export const ST = '\x9c'; + /** Operating System Command */ + export const OSC = '\x9d'; + /** Privacy Message */ + export const PM = '\x9e'; + /** Application Program Command */ + export const APC = '\x9f'; +} diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index eb7b70da99..ddb1678614 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -29,38 +29,39 @@ describe('InputHandler', () => { it('should call Terminal.setOption with correct params', () => { let terminal = new MockInputHandlingTerminal(); let inputHandler = new InputHandler(terminal); + const collect = ' '; - inputHandler.setCursorStyle([0]); + inputHandler.setCursorStyle([0], collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([1]); + inputHandler.setCursorStyle([1], collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([2]); + inputHandler.setCursorStyle([2], collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], false); terminal.options = {}; - inputHandler.setCursorStyle([3]); + inputHandler.setCursorStyle([3], collect); assert.equal(terminal.options['cursorStyle'], 'underline'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([4]); + inputHandler.setCursorStyle([4], collect); assert.equal(terminal.options['cursorStyle'], 'underline'); assert.equal(terminal.options['cursorBlink'], false); terminal.options = {}; - inputHandler.setCursorStyle([5]); + inputHandler.setCursorStyle([5], collect); assert.equal(terminal.options['cursorStyle'], 'bar'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([6]); + inputHandler.setCursorStyle([6], collect); assert.equal(terminal.options['cursorStyle'], 'bar'); assert.equal(terminal.options['cursorBlink'], false); }); @@ -68,14 +69,14 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle Terminal.bracketedPasteMode', () => { let terminal = new MockInputHandlingTerminal(); - terminal.prefix = '?'; + const collect = '?'; terminal.bracketedPasteMode = false; let inputHandler = new InputHandler(terminal); // Set bracketed paste mode - inputHandler.setMode([2004]); + inputHandler.setMode([2004], collect); assert.equal(terminal.bracketedPasteMode, true); // Reset bracketed paste mode - inputHandler.resetMode([2004]); + inputHandler.resetMode([2004], collect); assert.equal(terminal.bracketedPasteMode, false); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7696324ede..1e620e5f84 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,12 +4,104 @@ * @license MIT */ -import { CharData, IInputHandler, IInputHandlingTerminal } from './Types'; -import { C0 } from './EscapeSequences'; -import { DEFAULT_CHARSET } from './Charsets'; +import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { C0, C1 } from './EscapeSequences'; +import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; +import { EscapeSequenceParser } from './EscapeSequenceParser'; + +/** + * Map collect to glevel. Used in `selectCharset`. + */ +const GLEVEL = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2}; + + +/** + * DCS subparser implementations + */ + + /** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not supported + */ +class RequestTerminfo implements IDcsHandler { + private _data: string; + constructor(private _terminal: any) { } + hook(collect: string, params: number[], flag: number): void { + this._data = ''; + } + put(data: string, start: number, end: number): void { + this._data += data.substring(start, end); + } + unhook(): void { + // invalid: DCS 0 + r Pt ST + this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`); + } +} + +/** + * DCS $ q Pt ST + * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) + * Request Status String (DECRQSS), VT420 and up. + * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) + */ +class DECRQSS implements IDcsHandler { + private _data: string; + + constructor(private _terminal: any) { } + + hook(collect: string, params: number[], flag: number): void { + // reset data + this._data = ''; + } + + put(data: string, start: number, end: number): void { + this._data += data.substring(start, end); + } + + unhook(): void { + switch (this._data) { + // valid: DCS 1 $ r Pt ST (xterm) + case '"q': // DECSCA + return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`); + case '"p': // DECSCL + return this._terminal.send(`${C0.ESC}P1$r61"p${C0.ESC}\\`); + case 'r': // DECSTBM + let pt = '' + (this._terminal.buffer.scrollTop + 1) + + ';' + (this._terminal.buffer.scrollBottom + 1) + 'r'; + return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`); + case 'm': // SGR + // TODO: report real settings instead of 0m + return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`); + case ' q': // DECSCUSR + const STYLES = {'block': 2, 'underline': 4, 'bar': 6}; + let style = STYLES[this._terminal.getOption('cursorStyle')]; + style -= this._terminal.getOption('cursorBlink'); + return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); + default: + // invalid: DCS 0 $ r Pt ST (xterm) + this._terminal.error('Unknown DCS $q %s', this._data); + this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`); + } + } +} + +/** + * DCS Ps; Ps| Pt ST + * DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html) + * not supported + */ + + /** + * DCS + p Pt ST (xterm) + * Set Terminfo Data + * not supported + */ + + /** * The terminal's standard implementation of IInputHandler, this handles all @@ -19,24 +111,237 @@ import { wcwidth } from './CharWidth'; * each function's header comment. */ export class InputHandler implements IInputHandler { - constructor(private _terminal: IInputHandlingTerminal) { } + private _surrogateHigh: string; - public addChar(char: string, code: number): void { - if (char >= ' ') { + constructor( + private _terminal: any, // TODO: reestablish IInputHandlingTerminal here + private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) + { + this._surrogateHigh = ''; - // make buffer local for faster access - const buffer = this._terminal.buffer; + /** + * custom fallback handlers + */ + this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag)); + }); + this._parser.setEscHandlerFallback((collect: string, flag: number) => { + this._terminal.error('Unknown ESC code: ', collect, String.fromCharCode(flag)); + }); + this._parser.setExecuteHandlerFallback((code: number) => { + this._terminal.error('Unknown EXECUTE code: ', code); + }); + this._parser.setOscHandlerFallback((identifier: number, data: string) => { + this._terminal.error('Unknown OSC code: ', identifier, data); + }); + + /** + * print handler + */ + this._parser.setPrintHandler((data, start, end): void => 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)); + + /** + * execute handler + */ + this._parser.setExecuteHandler(C0.BEL, () => this.bell()); + this._parser.setExecuteHandler(C0.LF, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.VT, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.FF, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn()); + this._parser.setExecuteHandler(C0.BS, () => this.backspace()); + this._parser.setExecuteHandler(C0.HT, () => this.tab()); + this._parser.setExecuteHandler(C0.SO, () => this.shiftOut()); + this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); + // FIXME: What do to with missing? Old code just added those to print. + + // some C1 control codes - FIXME: should those be enabled by default? + this._parser.setExecuteHandler(C1.IND, () => this.index()); + this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); + this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); + + /** + * OSC handler + */ + // 0 - icon name + title + this._parser.setOscHandler(0, (data) => this.setTitle(data)); + // 1 - icon name + // 2 - title + this._parser.setOscHandler(2, (data) => this.setTitle(data)); + // 3 - set property X in the form "prop=value" + // 4 - Change Color Number + // 5 - Change Special Color Number + // 6 - Enable/disable Special Color Number c + // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) + // 10 - Change VT100 text foreground color to Pt. + // 11 - Change VT100 text background color to Pt. + // 12 - Change text cursor color to Pt. + // 13 - Change mouse foreground color to Pt. + // 14 - Change mouse background color to Pt. + // 15 - Change Tektronix foreground color to Pt. + // 16 - Change Tektronix background color to Pt. + // 17 - Change highlight background color to Pt. + // 18 - Change Tektronix cursor color to Pt. + // 19 - Change highlight foreground color to Pt. + // 46 - Change Log File to Pt. + // 50 - Set Font to Pt. + // 51 - reserved for Emacs shell. + // 52 - Manipulate Selection Data. + // 104 ; c - Reset Color Number c. + // 105 ; c - Reset Special Color Number c. + // 106 ; c; f - Enable/disable Special Color Number c. + // 110 - Reset VT100 text foreground color. + // 111 - Reset VT100 text background color. + // 112 - Reset text cursor color. + // 113 - Reset mouse foreground color. + // 114 - Reset mouse background color. + // 115 - Reset Tektronix foreground color. + // 116 - Reset Tektronix background color. + // 117 - Reset highlight color. + // 118 - Reset Tektronix cursor color. + // 119 - Reset highlight foreground color. + + /** + * 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()); + for (let 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? + } + + /** + * error handler + */ + this._parser.setErrorHandler((state) => { + this._terminal.error('Parsing error: ', state); + return state; + }); + + /** + * DCS handler + */ + this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); + this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); + } + + public parse(data: string): void { + let buffer = this._terminal.buffer; + const cursorStartX = buffer.x; + const cursorStartY = buffer.y; + if (this._terminal.debug) { + this._terminal.log('data: ' + data); + } + + // apply leftover surrogate high from last write + if (this._surrogateHigh) { + data = this._surrogateHigh + data; + this._surrogateHigh = ''; + } + + this._parser.parse(data); + + buffer = this._terminal.buffer; + if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + } + + public print(data: string, start: number, end: number): void { + let ch; + let code; + let low; + let chWidth; + const buffer = this._terminal.buffer; + for (let i = start; i < end; ++i) { + ch = data.charAt(i); + code = data.charCodeAt(i); + if (0xD800 <= code && code <= 0xDBFF) { + // we got a surrogate high + // get surrogate low (next 2 bytes) + low = data.charCodeAt(i + 1); + if (isNaN(low)) { + // end of data stream, save surrogate high + this._surrogateHigh = ch; + continue; + } + code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + ch += data.charAt(i + 1); + } + // surrogate low - already handled above + if (0xDC00 <= code && code <= 0xDFFF) { + continue; + } // calculate print space // expensive call, therefore we save width in line buffer - const chWidth = wcwidth(code); + chWidth = wcwidth(code); - if (this._terminal.charset && this._terminal.charset[char]) { - char = this._terminal.charset[char]; + if (this._terminal.charset && this._terminal.charset[ch]) { + ch = this._terminal.charset[ch]; } if (this._terminal.options.screenReaderMode) { - this._terminal.emit('a11y.char', char); + this._terminal.emit('a11y.char', ch); } let row = buffer.y + buffer.ybase; @@ -49,16 +354,16 @@ export class InputHandler implements IInputHandler { if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back if (buffer.lines.get(row)[buffer.x - 2]) { - buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += ch; + buffer.lines.get(row)[buffer.x - 2][3] = ch.charCodeAt(0); } } else { - buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += ch; + buffer.lines.get(row)[buffer.x - 1][3] = ch.charCodeAt(0); } this._terminal.updateRange(buffer.y); } - return; + continue; } // goto next line if ch would overflow @@ -78,7 +383,7 @@ export class InputHandler implements IInputHandler { } } else { if (chWidth === 2) { // FIXME: check for xterm behavior - return; + continue; } } } @@ -102,7 +407,7 @@ export class InputHandler implements IInputHandler { } } - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, ch, chWidth, ch.charCodeAt(0)]; buffer.x++; this._terminal.updateRange(buffer.y); @@ -551,19 +856,21 @@ export class InputHandler implements IInputHandler { /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: number[]): void { - let param = params[0] || 1; + public scrollDown(params: number[], collect?: string): void { + if (params.length < 2 && !collect) { + let param = params[0] || 1; - // make buffer local for faster access - const buffer = this._terminal.buffer; + // make buffer local for faster access + const buffer = this._terminal.buffer; - while (param--) { - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); + while (param--) { + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); + } + // this.maxRange(); + this._terminal.updateRange(buffer.scrollTop); + this._terminal.updateRange(buffer.scrollBottom); } - // this.maxRange(); - this._terminal.updateRange(buffer.scrollTop); - this._terminal.updateRange(buffer.scrollBottom); } /** @@ -687,18 +994,18 @@ export class InputHandler implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: number[]): void { + public sendDeviceAttributes(params: number[], collect?: string): void { if (params[0] > 0) { return; } - if (!this._terminal.prefix) { + if (!collect) { if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { this._terminal.send(C0.ESC + '[?1;2c'); } else if (this._terminal.is('linux')) { this._terminal.send(C0.ESC + '[?6c'); } - } else if (this._terminal.prefix === '>') { + } else if (collect === '>') { // xterm and urxvt // seem to spit this // out around ~370 times (?). @@ -874,7 +1181,7 @@ export class InputHandler implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: number[]): void { + public setMode(params: number[], collect?: string): void { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.setMode([params[i]]); @@ -883,7 +1190,7 @@ export class InputHandler implements IInputHandler { return; } - if (!this._terminal.prefix) { + if (!collect) { switch (params[0]) { case 4: this._terminal.insertMode = true; @@ -892,7 +1199,7 @@ export class InputHandler implements IInputHandler { // this._t.convertEol = true; break; } - } else if (this._terminal.prefix === '?') { + } else if (collect === '?') { switch (params[0]) { case 1: this._terminal.applicationCursor = true; @@ -1068,7 +1375,7 @@ export class InputHandler implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: number[]): void { + public resetMode(params: number[], collect?: string): void { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.resetMode([params[i]]); @@ -1077,7 +1384,7 @@ export class InputHandler implements IInputHandler { return; } - if (!this._terminal.prefix) { + if (!collect) { switch (params[0]) { case 4: this._terminal.insertMode = false; @@ -1086,7 +1393,7 @@ export class InputHandler implements IInputHandler { // this._t.convertEol = false; break; } - } else if (this._terminal.prefix === '?') { + } else if (collect === '?') { switch (params[0]) { case 1: this._terminal.applicationCursor = false; @@ -1369,8 +1676,8 @@ export class InputHandler implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: number[]): void { - if (!this._terminal.prefix) { + public deviceStatus(params: number[], collect?: string): void { + if (!collect) { switch (params[0]) { case 5: // status report @@ -1385,7 +1692,7 @@ export class InputHandler implements IInputHandler { + 'R'); break; } - } else if (this._terminal.prefix === '?') { + } else if (collect === '?') { // modern xterm doesnt seem to // respond to any of these except ?6, 6, and 5 switch (params[0]) { @@ -1421,21 +1728,23 @@ export class InputHandler implements IInputHandler { * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: number[]): 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; // ? - this._terminal.viewport.syncScrollArea(); - this._terminal.applicationCursor = false; - this._terminal.buffer.scrollTop = 0; - this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttr = this._terminal.defAttr; - this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + public softReset(params: number[], 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; // ? + this._terminal.viewport.syncScrollArea(); + this._terminal.applicationCursor = false; + this._terminal.buffer.scrollTop = 0; + this._terminal.buffer.scrollBottom = this._terminal.rows - 1; + this._terminal.curAttr = this._terminal.defAttr; + this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? + this._terminal.charset = null; + this._terminal.glevel = 0; // ?? + this._terminal.charsets = [null]; // ?? + } } /** @@ -1448,24 +1757,26 @@ export class InputHandler implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: number[]): void { - const param = params[0] < 1 ? 1 : params[0]; - switch (param) { - case 1: - case 2: - this._terminal.setOption('cursorStyle', 'block'); - break; - case 3: - case 4: - this._terminal.setOption('cursorStyle', 'underline'); - break; - case 5: - case 6: - this._terminal.setOption('cursorStyle', 'bar'); - break; + public setCursorStyle(params?: number[], collect?: string): void { + if (collect === ' ') { + const param = params[0] < 1 ? 1 : params[0]; + switch (param) { + case 1: + case 2: + this._terminal.setOption('cursorStyle', 'block'); + break; + case 3: + case 4: + this._terminal.setOption('cursorStyle', 'underline'); + break; + case 5: + case 6: + this._terminal.setOption('cursorStyle', 'bar'); + break; + } + const isBlinking = param % 2 === 1; + this._terminal.setOption('cursorBlink', isBlinking); } - const isBlinking = param % 2 === 1; - this._terminal.setOption('cursorBlink', isBlinking); } /** @@ -1474,8 +1785,8 @@ export class InputHandler implements IInputHandler { * dow) (DECSTBM). * CSI ? Pm r */ - public setScrollRegion(params: number[]): void { - if (this._terminal.prefix) return; + public setScrollRegion(params: number[], collect?: string): void { + if (collect) return; this._terminal.buffer.scrollTop = (params[0] || 1) - 1; this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1; this._terminal.buffer.x = 0; @@ -1485,6 +1796,7 @@ export class InputHandler implements IInputHandler { /** * CSI s + * ESC 7 * Save cursor (ANSI.SYS). */ public saveCursor(params: number[]): void { @@ -1495,10 +1807,147 @@ export class InputHandler implements IInputHandler { /** * CSI u + * ESC 8 * Restore cursor (ANSI.SYS). */ public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; } + + + /** + * OSC 0; ST (set icon name + window title) + * OSC 2; ST (set window title) + * Proxy to set window title. Icon name is not supported. + */ + public setTitle(data: string): void { + this._terminal.handleTitle(data); + } + + /** + * ESC E + * C1.NEL + * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL) + * Moves cursor to first position on next line. + */ + public nextLine(): void { + this._terminal.buffer.x = 0; + this.index(); + } + + /** + * ESC = + * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) + * Enables the numeric keypad to send application sequences to the host. + */ + public keypadApplicationMode(): void { + this._terminal.log('Serial port requested application keypad.'); + this._terminal.applicationKeypad = true; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + } + + /** + * ESC > + * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) + * Enables the keypad to send numeric characters to the host. + */ + public keypadNumericMode(): void { + this._terminal.log('Switching back to normal keypad.'); + this._terminal.applicationKeypad = false; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + } + + /** + * ESC % @ + * ESC % G + * Select default character set. UTF-8 is not supported (string are unicode anyways) + * therefore ESC % G does the same. + */ + public selectDefaultCharset(): void { + this._terminal.setgLevel(0); + this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + } + + /** + * ESC ( C + * Designate G0 Character Set, VT100, ISO 2022. + * ESC ) C + * Designate G1 Character Set (ISO 2022, VT100). + * ESC * C + * Designate G2 Character Set (ISO 2022, VT220). + * ESC + C + * Designate G3 Character Set (ISO 2022, VT220). + * ESC - C + * Designate G1 Character Set (VT300). + * ESC . C + * Designate G2 Character Set (VT300). + * ESC / C + * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? + */ + public selectCharset(collectAndFlag: string): void { + if (collectAndFlag.length !== 2) return this.selectDefaultCharset(); + if (collectAndFlag[0] === '/') return; // TODO: Is this supported? + this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + } + + /** + * ESC D + * C1.IND + * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html) + * Moves the cursor down one line in the same column. + */ + public index(): void { + this._terminal.index(); // TODO: save to move from terminal? + } + + /** + * ESC H + * C1.HTS + * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html) + * Sets a horizontal tab stop at the column position indicated by + * the value of the active column when the terminal receives an HTS. + */ + public tabSet(): void { + this._terminal.tabSet(); // TODO: save to move from terminal? + } + + /** + * ESC M + * C1.RI + * DEC mnemonic: HTS + * Moves the cursor up one line in the same column. If the cursor is at the top margin, + * the page scrolls down. + */ + public reverseIndex(): void { + this._terminal.reverseIndex(); // TODO: save to move from terminal? + } + + /** + * ESC c + * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) + * Reset to initial state. + */ + public reset(): void { + this._parser.reset(); + this._terminal.reset(); // TODO: save to move from terminal? + } + + /** + * ESC n + * ESC o + * ESC | + * ESC } + * ESC ~ + * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html) + * When you use a locking shift, the character set remains in GL or GR until + * you use another locking shift. (partly supported) + */ + public setgLevel(level: number): void { + this._terminal.setgLevel(level); // TODO: save to move from terminal? + } } diff --git a/src/Parser.ts b/src/Parser.ts index 372d844390..d3241307bf 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -232,7 +232,7 @@ export class Parser { if (ch in normalStateHandler) { normalStateHandler[ch](this, this._inputHandler); } else { - this._inputHandler.addChar(ch, code); + // this._inputHandler.addChar(ch, code); } break; case ParserState.ESCAPED: diff --git a/src/Terminal.ts b/src/Terminal.ts index fc3998d802..2cae7b56ed 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -32,7 +32,7 @@ import { Viewport } from './Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; -import { Parser } from './Parser'; +// import { Parser } from './Parser'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; @@ -195,8 +195,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public params: (string | number)[]; public currentParam: string | number; - public prefix: string; - public postfix: string; // user input states public writeBuffer: string[]; @@ -218,7 +216,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _inputHandler: InputHandler; public soundManager: SoundManager; - private _parser: Parser; public renderer: IRenderer; public selectionManager: SelectionManager; public linkifier: ILinkifier; @@ -321,8 +318,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.params = []; this.currentParam = 0; - this.prefix = ''; - this.postfix = ''; // user input states this.writeBuffer = []; @@ -333,7 +328,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._userScrolling = false; this._inputHandler = new InputHandler(this); - this._parser = new Parser(this._inputHandler, this); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -1318,8 +1312,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // middle of parsing escape sequence in two chunks. For some reason the // state of the parser resets to 0 after exiting parser.parse. This change // just sets the state back based on the correct return statement. - const state = this._parser.parse(data); - this._parser.setState(state); + + this._inputHandler.parse(data); this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); diff --git a/src/Types.ts b/src/Types.ts index 15498ce1f3..5075daa9bf 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -45,7 +45,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { bracketedPasteMode: boolean; defAttr: number; curAttr: number; - prefix: string; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -106,10 +105,11 @@ export interface ICompositionHelper { } /** - * Handles actions generated by the parser. + * Calls the parser and handles actions generated by the parser. */ export interface IInputHandler { - addChar(char: string, code: number): void; + parse(data: string): void; + print(data: string, start: number, end: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -135,26 +135,49 @@ export interface IInputHandler { /** CSI M */ deleteLines(params?: number[]): void; /** CSI P */ deleteChars(params?: number[]): void; /** CSI S */ scrollUp(params?: number[]): void; - /** CSI T */ scrollDown(params?: number[]): void; + /** CSI T */ scrollDown(params?: number[], collect?: string): void; /** CSI X */ eraseChars(params?: number[]): void; /** CSI Z */ cursorBackwardTab(params?: number[]): void; /** CSI ` */ charPosAbsolute(params?: number[]): void; /** CSI a */ HPositionRelative(params?: number[]): void; /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; - /** CSI c */ sendDeviceAttributes(params?: number[]): void; + /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void; /** CSI d */ linePosAbsolute(params?: number[]): void; /** CSI e */ VPositionRelative(params?: number[]): void; /** CSI f */ HVPosition(params?: number[]): void; /** CSI g */ tabClear(params?: number[]): void; - /** CSI h */ setMode(params?: number[]): void; - /** CSI l */ resetMode(params?: number[]): void; + /** CSI h */ setMode(params?: number[], collect?: string): void; + /** CSI l */ resetMode(params?: number[], collect?: string): void; /** CSI m */ charAttributes(params?: number[]): void; - /** CSI n */ deviceStatus(params?: number[]): void; - /** CSI p */ softReset(params?: number[]): void; - /** CSI q */ setCursorStyle(params?: number[]): void; - /** CSI r */ setScrollRegion(params?: number[]): void; + /** CSI n */ deviceStatus(params?: number[], collect?: string): void; + /** CSI p */ softReset(params?: number[], collect?: string): void; + /** CSI q */ setCursorStyle(params?: number[], collect?: string): void; + /** CSI r */ setScrollRegion(params?: number[], collect?: string): void; /** CSI s */ saveCursor(params?: number[]): void; /** CSI u */ restoreCursor(params?: number[]): void; + /** OSC 0 + OSC 2 */ setTitle(data: string): void; + /** ESC E */ nextLine(): void; + /** ESC = */ keypadApplicationMode(): void; + /** ESC > */ keypadNumericMode(): void; + /** ESC % G + ESC % @ */ selectDefaultCharset(): void; + /** ESC ( C + ESC ) C + ESC * C + ESC + C + ESC - C + ESC . C + ESC / C */ selectCharset(collectAndFlag: string): void; + /** ESC D */ index(): void; + /** ESC H */ tabSet(): void; + /** ESC M */ reverseIndex(): void; + /** ESC c */ reset(): void; + /** ESC n + ESC o + ESC | + ESC } + ESC ~ */ setgLevel(level: number): void; } export interface ILinkMatcher { @@ -226,7 +249,7 @@ export interface ILinkifierAccessor { } export interface IMouseHelper { - getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; } @@ -355,3 +378,134 @@ export interface IBrowser { export interface ISoundManager { playBellSound(): void; } + +/** + * Internal states of EscapeSequenceParser. + */ +export const enum ParserState { + GROUND = 0, + ESCAPE = 1, + ESCAPE_INTERMEDIATE = 2, + CSI_ENTRY = 3, + CSI_PARAM = 4, + CSI_INTERMEDIATE = 5, + CSI_IGNORE = 6, + SOS_PM_APC_STRING = 7, + OSC_STRING = 8, + DCS_ENTRY = 9, + DCS_PARAM = 10, + DCS_IGNORE = 11, + DCS_INTERMEDIATE = 12, + DCS_PASSTHROUGH = 13 +} + +/** +* Internal actions of EscapeSequenceParser. +*/ +export const enum ParserAction { + IGNORE = 0, + ERROR = 1, + PRINT = 2, + EXECUTE = 3, + OSC_START = 4, + OSC_PUT = 5, + OSC_END = 6, + CSI_DISPATCH = 7, + PARAM = 8, + COLLECT = 9, + ESC_DISPATCH = 10, + CLEAR = 11, + DCS_HOOK = 12, + DCS_PUT = 13, + DCS_UNHOOK = 14 +} + +/** + * Internal state of EscapeSequenceParser. + * Used as argument of the error handler to allow + * introspection at runtime on parse errors. + * Return it with altered values to recover from + * faulty states (not yet supported). + * Set `abort` to `true` to abort the current parsing. + */ +export interface IParsingState { + // position in parse string + position: number; + // actual character code + code: number; + // current parser state + currentState: ParserState; + // print buffer start index (-1 for not set) + print: number; + // dcs buffer start index (-1 for not set) + dcs: number; + // osc string buffer + osc: string; + // collect buffer with intermediate characters + collect: string; + // params buffer + params: number[]; + // should abort (default: false) + abort: boolean; +} + +/** +* DCS handler signature for EscapeSequenceParser. +* EscapeSequenceParser handles DCS commands via separate +* subparsers that get hook/unhooked and can handle +* arbitrary amount of print data. +* On entering a DSC sequence `hook` is called by +* `EscapeSequenceParser`. Use it to initialize or reset +* states needed to handle the current DCS sequence. +* EscapeSequenceParser will call `put` several times if the +* parsed string got splitted, therefore you might have to collect +* `data` until `unhook` is called. `unhook` marks the end +* of the current DCS sequence. +*/ +export interface IDcsHandler { + hook(collect: string, params: number[], flag: number): void; + put(data: string, start: number, end: number): void; + unhook(): void; +} + +/** +* EscapeSequenceParser interface. +*/ +export interface IEscapeSequenceParser { + /** + * Reset the parser to its initial state (handlers are kept). + */ + reset(): void; + + /** + * Parse string `data`. + * @param data The data to parse. + */ + parse(data: string): void; + + setPrintHandler(callback: (data: string, start: number, end: number) => void): void; + clearPrintHandler(): void; + + setExecuteHandler(flag: string, callback: () => void): void; + clearExecuteHandler(flag: string): void; + setExecuteHandlerFallback(callback: (code: number) => void): void; + + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; + clearCsiHandler(flag: string): void; + setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + + setEscHandler(collectAndFlag: string, callback: () => void): void; + clearEscHandler(collectAndFlag: string): void; + setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; + + setOscHandler(ident: number, callback: (data: string) => void): void; + clearOscHandler(ident: number): void; + setOscHandlerFallback(callback: (identifier: number, data: string) => void): void; + + setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; + clearDcsHandler(collectAndFlag: string): void; + setDcsHandlerFallback(handler: IDcsHandler): void; + + setErrorHandler(callback: (state: IParsingState) => IParsingState): void; + clearErrorHandler(): void; +} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 51aafb50f7..e3ae89d303 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -184,7 +184,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { bracketedPasteMode: boolean; defAttr: number; curAttr: number; - prefix: string; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; diff --git a/tslint.json b/tslint.json index ac1b9c959a..67dc795584 100644 --- a/tslint.json +++ b/tslint.json @@ -31,6 +31,7 @@ "parameter" ], "eofline": true, + "new-parens": true, "no-duplicate-imports": true, "no-eval": true, "no-internal-module": true,