diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index e9f8a9b4..4d3e8841 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -343,9 +343,12 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.term.length); - let scroll = result.row - terminal.buffer.viewportY; - scroll = scroll - Math.floor(terminal.rows / 2); - terminal.scrollLines(scroll); + // If it is not in the viewport then we scroll else it just gets selected + if (result.row > (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { + let scroll = result.row - terminal.buffer.viewportY; + scroll = scroll - Math.floor(terminal.rows / 2); + terminal.scrollLines(scroll); + } return true; } } diff --git a/demo/start.js b/demo/start.js index a054e939..a14627c1 100644 --- a/demo/start.js +++ b/demo/start.js @@ -20,9 +20,6 @@ startServer(); * For production builds see `webpack.config.js` in the root directory. If that is built the demo * can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for * validating that the packaged version works correctly. - * - * The addons are not webpacked right now and are built directly to `lib/` via `tsc` as they are - * the legacy format (`applyAddon`) and will be removed soon anyway. */ const clientConfig = { entry: path.resolve(__dirname, 'client.ts'), diff --git a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in index 571f2233..c56b99a0 100644 --- a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in +++ b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in @@ -27,5 +27,5 @@ r s t uvwxyz - + The end. diff --git a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text index 0940b073..940bc86d 100644 --- a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text +++ b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text @@ -1,25 +1,25 @@ -1 - 2 - 6 - 7 - 8 - 9 ABC +6 +7 +8 +9 ABC DEF a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p -yz qrstu vwx +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +yz vwx The end. + diff --git a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in index f9f6a424..a6aa1810 100644 --- a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in +++ b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in @@ -1,3 +1,3 @@ 1D2D3D4D5D6D7D8D9ABCDEFaDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDqDrDsDtDuvwxyz - + The end. diff --git a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text index f6644fb5..bc7344f9 100644 --- a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text +++ b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text @@ -1,4 +1,3 @@ - 2 6 7 8 @@ -21,5 +20,6 @@ DEF o p q -The end. rstu vwx +yz rstu vwx +The end. diff --git a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text index f89893ba..92c10331 100644 --- a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text +++ b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text @@ -1,4 +1,3 @@ - 6 C 8 ^^^^ 9 vvvv DL on line 11, expected: ACD_ 10 A @@ -13,14 +12,14 @@ 19 vvvv IL on line 21, expected: A_ 20 A - 22 ^^^^ -24 A -25 B -27 vvvv DL on line 28, expected: B_ +23 vvvv IL on line 24, expected: _A +25 B +26 ^^^^ 28 A + 29 B 30 ^^^^ 31 -32 \ No newline at end of file +32 diff --git a/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in new file mode 100644 index 00000000..b279f92a --- /dev/null +++ b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in @@ -0,0 +1,24 @@ +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x12 diff --git a/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text new file mode 100644 index 00000000..3acccf1a --- /dev/null +++ b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text @@ -0,0 +1,25 @@ +a +b +c +d +e +f +g +h +i +2 +k +l +m +n +o +p +q +r +1 +t +u +v +w +x + diff --git a/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in new file mode 100644 index 00000000..f818ad7f --- /dev/null +++ b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in @@ -0,0 +1,24 @@ +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x1 diff --git a/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text new file mode 100644 index 00000000..a2d66f8e --- /dev/null +++ b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text @@ -0,0 +1,25 @@ +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +1 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e533b709..1a662ec2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -21,8 +21,8 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; -import { OscHandlerFactory } from 'common/parser/OscParser'; -import { DcsHandlerFactory } from 'common/parser/DcsParser'; +import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from 'common/parser/DcsParser'; /** * Map collect to glevel. Used in `selectCharset`. @@ -245,10 +245,10 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC handler */ // 0 - icon name + title - this._parser.setOscHandler(0, new OscHandlerFactory((data: string) => this.setTitle(data))); + this._parser.setOscHandler(0, new OscHandler((data: string) => this.setTitle(data))); // 1 - icon name // 2 - title - this._parser.setOscHandler(2, new OscHandlerFactory((data: string) => this.setTitle(data))); + this._parser.setOscHandler(2, new OscHandler((data: string) => this.setTitle(data))); // 3 - set property X in the form "prop=value" // 4 - Change Color Number // 5 - Change Special Color Number @@ -433,10 +433,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (wraparoundMode) { buffer.x = 0; buffer.y++; - if (buffer.y > buffer.scrollBottom) { + if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; this._terminal.scroll(true); } else { + if (buffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; + } // The line already exists (eg. the initial viewport), mark it as a // wrapped line buffer.lines.get(buffer.y).isWrapped = true; @@ -506,7 +509,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Forward addDcsHandler from parser. */ public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - return this._parser.addDcsHandler(id, new DcsHandlerFactory(callback)); + return this._parser.addDcsHandler(id, new DcsHandler(callback)); } /** @@ -520,7 +523,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Forward addOscHandler from parser. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, new OscHandlerFactory(callback)); + return this._parser.addOscHandler(ident, new OscHandler(callback)); } /** @@ -543,9 +546,11 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.x = 0; } buffer.y++; - if (buffer.y > buffer.scrollBottom) { + if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; this._terminal.scroll(); + } else if (buffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; } // If the end of the line is hit, prevent this action from wrapping around to the next line. if (buffer.x >= this._bufferService.cols) { @@ -646,7 +651,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Up Ps Times (default = 1) (CUU). */ public cursorUp(params: IParams): void { - this._moveCursor(0, -(params.params[0] || 1)); + // stop at scrollTop + const diffToTop = this._bufferService.buffer.y - this._bufferService.buffer.scrollTop; + if (diffToTop >= 0) { + this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1)); + } else { + this._moveCursor(0, -(params.params[0] || 1)); + } } /** @@ -654,7 +665,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Down Ps Times (default = 1) (CUD). */ public cursorDown(params: IParams): void { - this._moveCursor(0, params.params[0] || 1); + // stop at scrollBottom + const diffToBottom = this._bufferService.buffer.scrollBottom - this._bufferService.buffer.y; + if (diffToBottom >= 0) { + this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1)); + } else { + this._moveCursor(0, params.params[0] || 1); + } } /** @@ -679,7 +696,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Other than cursorDown (CUD) also set the cursor to first column. */ public cursorNextLine(params: IParams): void { - this._moveCursor(0, params.params[0] || 1); + this.cursorDown(params); this._bufferService.buffer.x = 0; } @@ -689,7 +706,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Other than cursorUp (CUU) also set the cursor to first column. */ public cursorPrecedingLine(params: IParams): void { - this._moveCursor(0, -(params.params[0] || 1)); + this.cursorUp(params); this._bufferService.buffer.x = 0; } @@ -1031,7 +1048,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } @@ -1633,7 +1650,6 @@ export class InputHandler extends Disposable implements IInputHandler { } } - /** * Helper to extract and apply color params/subparams. * Returns advance for params index. @@ -1906,6 +1922,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + public deviceStatusPrivate(params: IParams): void { // modern xterm doesnt seem to // respond to any of these except ?6, 6, and 5 @@ -2134,10 +2151,13 @@ export class InputHandler extends Disposable implements IInputHandler { */ public index(): void { this._restrictCursor(); + const buffer = this._bufferService.buffer; this._bufferService.buffer.y++; - if (this._bufferService.buffer.y > this._bufferService.buffer.scrollBottom) { - this._bufferService.buffer.y--; + if (buffer.y === buffer.scrollBottom + 1) { + buffer.y--; this._terminal.scroll(); + } else if (buffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; } this._restrictCursor(); } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 714257f9..e0f9f878 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -1123,6 +1123,241 @@ describe('Terminal', () => { }); }); }); + + describe('Buffer.stringIndexToBufferIndex', () => { + let terminal: TestTerminal; + + beforeEach(() => { + terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + }); + + it('multiline ascii', () => { + const input = 'This is ASCII text spanning multiple lines.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + }); + + it('combining e\u0301 in a sentence', () => { + const input = 'Sitting in the cafe\u0301 drinking coffee.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 19; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 18), + terminal.buffer.stringIndexToBufferIndex(0, 19)); + // after the combining char every string index has an offset of -1 + for (let i = 19; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline combining e\u0301', () => { + const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 2 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + it('surrogate char in a sentence', () => { + const input = 'The 𝄞 is a clef widely used in modern notation.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 5; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 4), + terminal.buffer.stringIndexToBufferIndex(0, 5)); + // after the combining char every string index has an offset of -1 + for (let i = 5; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate char', () => { + const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 2 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + it('surrogate char with combining', () => { + // eye of Ra with acute accent - string length of 3 + const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // index 0..2 should map to 0 + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); + for (let i = 2; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate with combining', () => { + const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 3 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth chars', () => { + const input = 'These 123 are some fat numbers.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 6; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 6, 7, 8 take 2 cells + assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); + assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); + // rest of the string has offset of +3 + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); + } + }); + + it('multiline fullwidth chars', () => { + const input = '12345678901234567890'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth combining with emoji - match emoji cell', () => { + const input = 'Lots of ¥\u0301 make me 😃.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + const stringIndex = s.match(/😃/).index; + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); + assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); + }); + + it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { + const input = 'a12345678901234567890'; + // the 'a' at the beginning moves all fullwidth chars one to the right + // now the end of the line contains a dangling empty cell since + // the next fullwidth char has to wrap early + // the dangling last cell is wrongly added in the string + // --> fixable after resolving #1685 + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 10; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + const j = (i - 0) << 1; + assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); + } + }); + + it('test fully wrapped buffer up to last char', () => { + const input = Array(6).join('1234567890'); + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + it('test fully wrapped buffer up to last char with full width odd', () => { + const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' + + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + assert.equal( + (!(i % 3)) + ? input[i] + : (i % 3 === 1) + ? input.substr(i, 2) + : input.substr(i - 1, 2), + terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + it('should handle \t in lines correctly', () => { + const input = '\thttps://google.de'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); + }); + }); + + describe('BufferStringIterator', function(): void { + it('iterator does not overflow buffer limits', function(): void { + const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + const data = [ + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa' + ]; + terminal.writeSync(data.join('')); + // brute force test with insane values + expect(() => { + for (let overscan = 0; overscan < 20; ++overscan) { + for (let start = -10; start < 20; ++start) { + for (let end = -10; end < 20; ++end) { + const it = terminal.buffer.iterator(false, start, end, overscan, overscan); + while (it.hasNext()) { + it.next(); + } + } + } + } + }).to.not.throw(); + }); + }); }); class TestLinkifier extends Linkifier { diff --git a/src/Terminal.ts b/src/Terminal.ts index 2a1a4cf6..90c4b286 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -394,6 +394,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp case 'rendererType': if (this._renderService) { this._renderService.setRenderer(this._createRenderer()); + this._renderService.onResize(this.cols, this.rows); } break; case 'scrollback': @@ -643,7 +644,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.screenElement); this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); - this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this._selectionService.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it @@ -664,10 +664,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); + // This event listener must be registered aftre MouseZoneManager is created + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); + // apply mouse event classes set by escape codes before terminal was attached - this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { this._selectionService.disable(); + this.element.classList.add('enable-mouse-events'); } else { this._selectionService.enable(); } diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index cbd790d5..efb2a446 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -17,12 +17,6 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const SKIP_FILES = [ - 't0070-DECSTBM_LF.in', // lineFeed not working correctly - 't0071-DECSTBM_IND.in', - 't0072-DECSTBM_NEL.in', - 't0075-DECSTBM_CUU_CUD.in', - 't0076-DECSTBM_IL_DL.in', // not working due to lineFeed - 't0077-DECSTBM_quirks.in', 't0084-CBT.in', 't0101-NLM.in', 't0103-reverse_wrap.in', @@ -42,7 +36,7 @@ const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slic describe('Escape Sequence Files', function(): void { - this.timeout(20000); + this.timeout(1000); let ptyTerm: any; let slaveEnd: any; diff --git a/src/Types.d.ts b/src/Types.d.ts index 25824818..f591fa4f 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -116,7 +116,7 @@ export interface IInputHandler { /** CSI a */ hPositionRelative(params: IParams): void; /** CSI b */ repeatPrecedingCharacter(params: IParams): void; /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void; - sendDeviceAttributesSecondary(params: IParams): void; + /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void; /** CSI d */ linePosAbsolute(params: IParams): void; /** CSI e */ vPositionRelative(params: IParams): void; /** CSI f */ hVPosition(params: IParams): void; diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index c0b3c0e5..63084f54 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -108,7 +108,7 @@ export class RenderService extends Disposable implements IRenderService { } public setRenderer(renderer: IRenderer): void { - // TODO: RenderCoordinator should be the only one to dispose the renderer + // TODO: RenderService should be the only one to dispose the renderer this._renderer.dispose(); this._renderer = renderer; this.refreshRows(0, this._rowCount - 1); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index e4c703eb..90c85ecd 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -563,9 +563,10 @@ export class SelectionService implements ISelectionService { // to be sent to the pty. event.stopImmediatePropagation(); - // Something went wrong + // Do nothing if there is no selection start, this can happen if the first + // click in the terminal is an incremental click if (!this._model.selectionStart) { - throw new Error('Selection start position was not set before mousemove event'); + return; } // Record the previous position so we know whether to redraw the selection diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index af5abeca..35aade4a 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -1163,237 +1163,4 @@ describe('Buffer', () => { assert.equal(str3, '😁a'); }); }); - // describe('stringIndexToBufferIndex', () => { - // let terminal: TestTerminal; - - // beforeEach(() => { - // terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - // }); - - // it('multiline ascii', () => { - // const input = 'This is ASCII text spanning multiple lines.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // }); - - // it('combining e\u0301 in a sentence', () => { - // const input = 'Sitting in the cafe\u0301 drinking coffee.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < 19; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index - // assert.deepEqual( - // terminal.buffer.stringIndexToBufferIndex(0, 18), - // terminal.buffer.stringIndexToBufferIndex(0, 19)); - // // after the combining char every string index has an offset of -1 - // for (let i = 19; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline combining e\u0301', () => { - // const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // every buffer cell index contains 2 string indices - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('surrogate char in a sentence', () => { - // const input = 'The 𝄞 is a clef widely used in modern notation.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < 5; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index - // assert.deepEqual( - // terminal.buffer.stringIndexToBufferIndex(0, 4), - // terminal.buffer.stringIndexToBufferIndex(0, 5)); - // // after the combining char every string index has an offset of -1 - // for (let i = 5; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline surrogate char', () => { - // const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // every buffer cell index contains 2 string indices - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('surrogate char with combining', () => { - // // eye of Ra with acute accent - string length of 3 - // const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // index 0..2 should map to 0 - // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); - // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); - // for (let i = 2; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline surrogate with combining', () => { - // const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // every buffer cell index contains 3 string indices - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); - // } - // }); - - // it('fullwidth chars', () => { - // const input = 'These 123 are some fat numbers.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < 6; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // // string index 6, 7, 8 take 2 cells - // assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); - // assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); - // // rest of the string has offset of +3 - // for (let i = 9; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline fullwidth chars', () => { - // const input = '12345678901234567890'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 9; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('fullwidth combining with emoji - match emoji cell', () => { - // const input = 'Lots of ¥\u0301 make me 😃.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // const stringIndex = s.match(/😃/).index; - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - // assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); - // }); - - // it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { - // const input = 'a12345678901234567890'; - // // the 'a' at the beginning moves all fullwidth chars one to the right - // // now the end of the line contains a dangling empty cell since - // // the next fullwidth char has to wrap early - // // the dangling last cell is wrongly added in the string - // // --> fixable after resolving #1685 - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 10; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - // const j = (i - 0) << 1; - // assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); - // } - // }); - - // it('test fully wrapped buffer up to last char', () => { - // const input = Array(6).join('1234567890'); - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - // assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - // } - // }); - - // it('test fully wrapped buffer up to last char with full width odd', () => { - // const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' - // + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - // assert.equal( - // (!(i % 3)) - // ? input[i] - // : (i % 3 === 1) - // ? input.substr(i, 2) - // : input.substr(i - 1, 2), - // terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - // } - // }); - - // it('should handle \t in lines correctly', () => { - // const input = '\thttps://google.de'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(s, Array(optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); - // }); - // }); - // describe('BufferStringIterator', function(): void { - // it('iterator does not overflow buffer limits', function(): void { - // const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - // const data = [ - // 'aaaaaaaaaa', - // 'aaaaaaaaa\n', - // 'aaaaaaaaaa', - // 'aaaaaaaaa\n', - // 'aaaaaaaaaa', - // 'aaaaaaaaaa', - // 'aaaaaaaaaa', - // 'aaaaaaaaa\n', - // 'aaaaaaaaaa', - // 'aaaaaaaaaa' - // ]; - // terminal.writeSync(data.join('')); - // // brute force test with insane values - // expect(() => { - // for (let overscan = 0; overscan < 20; ++overscan) { - // for (let start = -10; start < 20; ++start) { - // for (let end = -10; end < 20; ++end) { - // const it = terminal.buffer.iterator(false, start, end, overscan, overscan); - // while (it.hasNext()) { - // it.next(); - // } - // } - // } - // } - // }).to.not.throw(); - // }); - // }); }); diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 532230ec..e229d69c 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -40,7 +40,7 @@ export interface IBuffer { nextStop(x?: number): number; prevStop(x?: number): number; getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; - stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; getNullCell(attr?: IAttributeData): ICellData; getWhitespaceCell(attr?: IAttributeData): ICellData; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index 3ef42a68..4d6dce11 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { DcsParser, DcsHandlerFactory } from 'common/parser/DcsParser'; +import { DcsParser, DcsHandler } from 'common/parser/DcsParser'; import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; @@ -75,7 +75,7 @@ describe('DcsParser', () => { beforeEach(() => { reports = []; parser = new DcsParser(); - parser.setDcsHandlerFallback((id, action, data) => { + parser.setHandlerFallback((id, action, data) => { if (action === 'HOOK') { data = data.toArray(); } @@ -84,7 +84,7 @@ describe('DcsParser', () => { }); describe('handler registration', () => { it('setDcsHandler', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -100,8 +100,8 @@ describe('DcsParser', () => { ]); }); it('clearDcsHandler', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); - parser.clearDcsHandler(identifier({intermediates: '+', final: 'p'})); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.clearHandler(identifier({intermediates: '+', final: 'p'})); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -117,8 +117,8 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -137,8 +137,8 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler with return false', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -157,8 +157,8 @@ describe('DcsParser', () => { ]); }); it('dispose handlers', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); dispo.dispose(); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); @@ -176,7 +176,7 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -186,7 +186,7 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -196,8 +196,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); - const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['two', params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -215,8 +215,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); it('should respect return false', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); + parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -227,7 +227,7 @@ describe('DcsParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); const data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { @@ -238,7 +238,7 @@ describe('DcsParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 0112e935..4622c4ad 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -4,25 +4,25 @@ */ import { IDisposable } from 'common/Types'; -import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; +const EMPTY_HANDLERS: IDcsHandler[] = []; export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); - private _empty: IDcsHandler[] = []; - private _active: IDcsHandler[] = this._empty; + private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; - private _handlerFb: DcsFallbackHandler = () => {}; + private _handlerFb: DcsFallbackHandlerType = () => {}; public dispose(): void { this._handlers = Object.create(null); this._handlerFb = () => {}; } - public addDcsHandler(ident: number, handler: IDcsHandler): IDisposable { + public addHandler(ident: number, handler: IDcsHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -38,15 +38,15 @@ export class DcsParser implements IDcsParser { }; } - public setDcsHandler(ident: number, handler: IDcsHandler): void { + public setHandler(ident: number, handler: IDcsHandler): void { this._handlers[ident] = [handler]; } - public clearDcsHandler(ident: number): void { + public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - public setDcsHandlerFallback(handler: DcsFallbackHandler): void { + public setHandlerFallback(handler: DcsFallbackHandlerType): void { this._handlerFb = handler; } @@ -54,7 +54,7 @@ export class DcsParser implements IDcsParser { if (this._active.length) { this.unhook(false); } - this._active = this._empty; + this._active = EMPTY_HANDLERS; this._ident = 0; } @@ -62,7 +62,7 @@ export class DcsParser implements IDcsParser { // always reset leftover handlers this.reset(); this._ident = ident; - this._active = this._handlers[ident] || this._empty; + this._active = this._handlers[ident] || EMPTY_HANDLERS; if (!this._active.length) { this._handlerFb(this._ident, 'HOOK', params); } else { @@ -98,7 +98,7 @@ export class DcsParser implements IDcsParser { this._active[j].unhook(false); } } - this._active = this._empty; + this._active = EMPTY_HANDLERS; this._ident = 0; } } @@ -107,7 +107,7 @@ export class DcsParser implements IDcsParser { * Convenient class to create a DCS handler from a single callback function. * Note: The payload is currently limited to 50 MB (hardcoded). */ -export class DcsHandlerFactory implements IDcsHandler { +export class DcsHandler implements IDcsHandler { private _data = ''; private _params: IParams | undefined; private _hitLimit: boolean = false; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 40ce38b1..b9a5ae4e 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,15 +3,15 @@ * @license MIT */ -import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; import { ParserState } from 'common/parser/Constants'; import { Params } from 'common/parser/Params'; -import { OscHandlerFactory } from 'common/parser/OscParser'; +import { OscHandler } from 'common/parser/OscParser'; import { IDisposable } from 'common/Types'; -import { DcsHandlerFactory } from 'common/parser/DcsParser'; +import { DcsHandler } from 'common/parser/DcsParser'; function r(a: number, b: number): string[] { @@ -24,7 +24,7 @@ function r(a: number, b: number): string[] { } class MockOscPutParser implements IOscParser { - private _fallback: OscFallbackHandler = () => {}; + private _fallback: OscFallbackHandlerType = () => {}; public data = ''; public reset(): void { this.data = ''; @@ -41,16 +41,16 @@ class MockOscPutParser implements IOscParser { this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1)); } } - addOscHandler(ident: number, handler: IOscHandler): IDisposable { + addHandler(ident: number, handler: IOscHandler): IDisposable { throw new Error('not implemented'); } - setOscHandler(ident: number, handler: IOscHandler): void { + setHandler(ident: number, handler: IOscHandler): void { throw new Error('not implemented'); } - clearOscHandler(ident: number): void { + clearHandler(ident: number): void { throw new Error('not implemented'); } - setOscHandlerFallback(handler: OscFallbackHandler): void { + setHandlerFallback(handler: OscFallbackHandlerType): void { this._fallback = handler; } } @@ -1220,26 +1220,26 @@ describe('EscapeSequenceParser', function (): void { }); describe('ESC custom handlers', () => { it('prevent fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G']); }); it('allow fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G', 'default - %G']); }); it('Multiple custom handlers fallback once', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom2 - %G', 'custom - %G']); }); it('Multiple custom handlers no fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); parse(parser2, INPUT); @@ -1247,21 +1247,21 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => order.push(1)); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; }); parse(parser2, '\x1b%G'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); parse(parser2, INPUT); chai.expect(esc).eql(['default - %G']); }); it('Should not corrupt the parser when dispose is called twice', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); dispo.dispose(); @@ -1284,7 +1284,7 @@ describe('EscapeSequenceParser', function (): void { describe('CSI custom handlers', () => { it('Prevent fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); @@ -1292,7 +1292,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Allow fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); @@ -1301,7 +1301,7 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); @@ -1312,7 +1312,7 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); @@ -1322,7 +1322,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setCsiHandler({final: 'm'}, () => order.push(1)); + parser2.setCsiHandler({final: 'm'}, () => { order.push(1); }); parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; }); parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); @@ -1330,7 +1330,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Dispose should work', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); @@ -1339,7 +1339,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); @@ -1364,7 +1364,7 @@ describe('EscapeSequenceParser', function (): void { chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { - parser2.setOscHandler(1, new OscHandlerFactory(function (data: string): void { + parser2.setOscHandler(1, new OscHandler(function (data: string): void { osc.push([1, data]); })); parse(parser2, INPUT); @@ -1378,16 +1378,16 @@ describe('EscapeSequenceParser', function (): void { describe('OSC custom handlers', () => { it('Prevent fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); it('Allow fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return false; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return false; })); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); @@ -1395,9 +1395,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return false; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return false; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); @@ -1406,9 +1406,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return true; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([], 'Should not fallback once'); @@ -1416,16 +1416,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setOscHandler(1, new OscHandlerFactory(() => order.push(1))); - parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(2); return false; })); - parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(3); return false; })); + parser2.setOscHandler(1, new OscHandler(() => order.push(1))); + parser2.addOscHandler(1, new OscHandler(() => { order.push(2); return false; })); + parser2.addOscHandler(1, new OscHandler(() => { order.push(3); return false; })); parse(parser2, '\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); @@ -1433,8 +1433,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1476,54 +1476,54 @@ describe('EscapeSequenceParser', function (): void { const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; it('Prevent fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]); }); it('Allow fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers fallback once', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers no fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => order.push(1))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(2); return false; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(3); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => order.push(1))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; })); parse(parser2, DCS_INPUT); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); }); it('Should not corrupt the parser when dispose is called twice', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); dispo.dispose(); parse(parser2, DCS_INPUT); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 30aabc6d..55bac11e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler, IFunctionIdentifier } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -238,19 +238,19 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: number; // handler lookup containers - protected _printHandler: (data: Uint32Array, start: number, end: number) => void; - protected _executeHandlers: any; - protected _csiHandlers: IHandlerCollection; - protected _escHandlers: IHandlerCollection; + protected _printHandler: PrintHandlerType; + protected _executeHandlers: {[flag: number]: ExecuteHandlerType}; + protected _csiHandlers: IHandlerCollection; + protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; protected _dcsParser: IDcsParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; - protected _executeHandlerFb: (code: number) => void; - protected _csiHandlerFb: (ident: number, params: IParams) => void; - protected _escHandlerFb: (ident: number) => void; + protected _printHandlerFb: PrintFallbackHandlerType; + protected _executeHandlerFb: ExecuteFallbackHandlerType; + protected _csiHandlerFb: CsiFallbackHandlerType; + protected _escHandlerFb: EscFallbackHandlerType; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -328,47 +328,74 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } public dispose(): void { - this._executeHandlers = null; + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); this._escHandlers = Object.create(null); this._oscParser.dispose(); this._dcsParser.dispose(); } - public setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { - this._printHandler = callback; + public setPrintHandler(handler: PrintHandlerType): void { + this._printHandler = handler; } public clearPrintHandler(): void { this._printHandler = this._printHandlerFb; } - public setExecuteHandler(flag: string, callback: () => void): void { - this._executeHandlers[flag.charCodeAt(0)] = callback; - } - public clearExecuteHandler(flag: string): void { - if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; - } - public setExecuteHandlerFallback(callback: (code: number) => void): void { - this._executeHandlerFb = callback; - } - - public addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { - const ident = this._identifier(id); - if (this._csiHandlers[ident] === undefined) { - this._csiHandlers[ident] = []; + public addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { + const ident = this._identifier(id, [0x30, 0x7e]); + if (this._escHandlers[ident] === undefined) { + this._escHandlers[ident] = []; } - const handlerList = this._csiHandlers[ident]; - handlerList.push(callback); + const handlerList = this._escHandlers[ident]; + handlerList.push(handler); return { dispose: () => { - const handlerIndex = handlerList.indexOf(callback); + const handlerIndex = handlerList.indexOf(handler); if (handlerIndex !== -1) { handlerList.splice(handlerIndex, 1); } } }; } - public setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { - this._csiHandlers[this._identifier(id)] = [callback]; + public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void { + this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler]; + } + public clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; + } + public setEscHandlerFallback(handler: EscFallbackHandlerType): void { + this._escHandlerFb = handler; + } + + public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void { + this._executeHandlers[flag.charCodeAt(0)] = handler; + } + public clearExecuteHandler(flag: string): void { + if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; + } + public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void { + this._executeHandlerFb = handler; + } + + public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { + const ident = this._identifier(id); + if (this._csiHandlers[ident] === undefined) { + this._csiHandlers[ident] = []; + } + const handlerList = this._csiHandlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void { + this._csiHandlers[this._identifier(id)] = [handler]; } public clearCsiHandler(id: IFunctionIdentifier): void { if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; @@ -377,56 +404,30 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._csiHandlerFb = callback; } - public addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { - const ident = this._identifier(id, [0x30, 0x7e]); - if (this._escHandlers[ident] === undefined) { - this._escHandlers[ident] = []; - } - const handlerList = this._escHandlers[ident]; - handlerList.push(callback); - return { - dispose: () => { - const handlerIndex = handlerList.indexOf(callback); - if (handlerIndex !== -1) { - handlerList.splice(handlerIndex, 1); - } - } - }; + public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.addHandler(this._identifier(id), handler); } - public setEscHandler(id: IFunctionIdentifier, callback: () => void): void { - this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [callback]; + public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { + this._dcsParser.setHandler(this._identifier(id), handler); } - public clearEscHandler(id: IFunctionIdentifier): void { - if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; + public clearDcsHandler(id: IFunctionIdentifier): void { + this._dcsParser.clearHandler(this._identifier(id)); } - public setEscHandlerFallback(callback: (ident: number) => void): void { - this._escHandlerFb = callback; + public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void { + this._dcsParser.setHandlerFallback(handler); } public addOscHandler(ident: number, handler: IOscHandler): IDisposable { - return this._oscParser.addOscHandler(ident, handler); + return this._oscParser.addHandler(ident, handler); } public setOscHandler(ident: number, handler: IOscHandler): void { - this._oscParser.setOscHandler(ident, handler); + this._oscParser.setHandler(ident, handler); } public clearOscHandler(ident: number): void { - this._oscParser.clearOscHandler(ident); + this._oscParser.clearHandler(ident); } - public setOscHandlerFallback(handler: OscFallbackHandler): void { - this._oscParser.setOscHandlerFallback(handler); - } - - public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { - return this._dcsParser.addDcsHandler(this._identifier(id), handler); - } - public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { - this._dcsParser.setDcsHandler(this._identifier(id), handler); - } - public clearDcsHandler(id: IFunctionIdentifier): void { - this._dcsParser.clearDcsHandler(this._identifier(id)); - } - public setDcsHandlerFallback(handler: DcsFallbackHandler): void { - this._dcsParser.setDcsHandlerFallback(handler); + public setOscHandlerFallback(handler: OscFallbackHandlerType): void { + this._oscParser.setHandlerFallback(handler); } public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { @@ -443,7 +444,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; - // this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index 2d1180d2..6969d6f3 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { OscParser, OscHandlerFactory } from 'common/parser/OscParser'; +import { OscParser, OscHandler } from 'common/parser/OscParser'; import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder'; import { IOscHandler } from 'common/parser/Types'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -37,7 +37,7 @@ describe('OscParser', () => { beforeEach(() => { reports = []; parser = new OscParser(); - parser.setOscHandlerFallback((id, action, data) => { + parser.setHandlerFallback((id, action, data) => { reports.push([id, action, data]); }); }); @@ -78,7 +78,7 @@ describe('OscParser', () => { }); describe('handler registration', () => { it('setOscHandler', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th')); + parser.setHandler(1234, new TestHandler(1234, reports, 'th')); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -94,8 +94,8 @@ describe('OscParser', () => { ]); }); it('clearOscHandler', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th')); - parser.clearOscHandler(1234); + parser.setHandler(1234, new TestHandler(1234, reports, 'th')); + parser.clearHandler(1234); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -111,8 +111,8 @@ describe('OscParser', () => { ]); }); it('addOscHandler', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); - parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2')); + parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.addHandler(1234, new TestHandler(1234, reports, 'th2')); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -131,8 +131,8 @@ describe('OscParser', () => { ]); }); it('addOscHandler with return false', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); - parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true)); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -151,8 +151,8 @@ describe('OscParser', () => { ]); }); it('dispose handlers', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); - const dispo = parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); + const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true)); dispo.dispose(); parser.start(); let data = toUtf32('1234;Here comes'); @@ -170,7 +170,7 @@ describe('OscParser', () => { }); describe('OscHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -180,7 +180,7 @@ describe('OscParser', () => { assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -190,8 +190,8 @@ describe('OscParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data]))); - const dispo = parser.addOscHandler(1234, new OscHandlerFactory(data => reports.push(['two', data]))); + parser.setHandler(1234, new OscHandler(data => reports.push(['one', data]))); + const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -209,8 +209,8 @@ describe('OscParser', () => { assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); }); it('should respect return false', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data]))); - parser.addOscHandler(1234, new OscHandlerFactory(data => { reports.push(['two', data]); return false; })); + parser.setHandler(1234, new OscHandler(data => reports.push(['one', data]))); + parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -221,7 +221,7 @@ describe('OscParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); @@ -234,7 +234,7 @@ describe('OscParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index ed549deb..e8c5a801 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -3,20 +3,19 @@ * @license MIT */ -import { IOscHandler, IHandlerCollection, OscFallbackHandler } from 'common/parser/Types'; +import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types'; import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; -import { Disposable } from 'common/Lifecycle'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; -export class OscParser extends Disposable { +export class OscParser implements IOscParser { private _state = OscState.START; private _id = -1; private _handlers: IHandlerCollection = Object.create(null); - private _handlerFb: OscFallbackHandler = () => { }; + private _handlerFb: OscFallbackHandlerType = () => { }; - public addOscHandler(ident: number, handler: IOscHandler): IDisposable { + public addHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -31,13 +30,13 @@ export class OscParser extends Disposable { } }; } - public setOscHandler(ident: number, handler: IOscHandler): void { + public setHandler(ident: number, handler: IOscHandler): void { this._handlers[ident] = [handler]; } - public clearOscHandler(ident: number): void { + public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - public setOscHandlerFallback(handler: OscFallbackHandler): void { + public setHandlerFallback(handler: OscFallbackHandlerType): void { this._handlerFb = handler; } @@ -168,7 +167,7 @@ export class OscParser extends Disposable { * Convenient class to allow attaching string based handler functions * as OSC handlers. */ -export class OscHandlerFactory implements IOscHandler { +export class OscHandler implements IOscHandler { private _data = ''; private _hitLimit: boolean = false; diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index b237cd10..e2fac89f 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -61,65 +61,84 @@ export interface IParsingState { abort: boolean; } -export interface IHandlerCollection { - [key: string]: T[]; -} - -export type CsiHandler = (params: IParams) => boolean | void; -export type EscHandler = () => boolean | void; +/** + * Command handler interfaces. + */ /** -* DCS handler signature for EscapeSequenceParser. -* EscapeSequenceParser handles DCS commands via separate -* subparsers that get hook/unhooked and can handle -* arbitrary amount of 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. -* Note: A DCS parser is only instantiated once, therefore -* you cannot rely on the ctor to reinitialize state. -* -* EscapeSequenceParser will call `put` several times if the -* parsed data got split, therefore you might have to collect -* `data` until `unhook` is called. -* Note: `data` is borrowed, if you cannot process the data -* in chunks you have to copy it, doing otherwise will lead to -* data losses or corruption. -* -* `unhook` marks the end of the current DCS sequence. `success` -* indicates whether the command was aborted. -*/ + * CSI handler types. + * Note: `params` is borrowed. + */ +export type CsiHandlerType = (params: IParams) => boolean | void; +export type CsiFallbackHandlerType = (ident: number, params: IParams) => void; + +/** + * DCS handler types. + */ export interface IDcsHandler { + /** + * Called when a DCS command starts. + * Prepare needed data structures here. + * Note: `params` is borrowed. + */ hook(params: IParams): void; + /** + * Incoming payload chunk. + * Note: `params` is borrowed. + */ put(data: Uint32Array, start: number, end: number): void; + /** + * End of DCS command. `success` indicates whether the + * command finished normally or got aborted, thus final + * execution of the command should depend on `success`. + * To save memory also cleanup data structures here. + */ unhook(success: boolean): void | boolean; } +export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; -export type DcsFallbackHandler = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; +/** + * ESC handler types. + */ +export type EscHandlerType = () => boolean | void; +export type EscFallbackHandlerType = (identifier: number) => void; +/** + * EXECUTE handler types. + */ +export type ExecuteHandlerType = () => boolean | void; +export type ExecuteFallbackHandlerType = (ident: number) => void; + +/** + * OSC handler types. + */ export interface IOscHandler { /** * Announces start of this OSC command. * Prepare needed data structures here. */ start(): void; - /** * Incoming data chunk. + * Note: Data is borrowed. */ put(data: Uint32Array, start: number, end: number): void; - /** * End of OSC command. `success` indicates whether the - * command finished normally or got aborted, thus execution - * of the command should depend on `success`. - * To save memory cleanup data structures in `.end`. + * command finished normally or got aborted, thus final + * execution of the command should depend on `success`. + * To save memory also cleanup data structures here. */ end(success: boolean): void | boolean; } +export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; + +/** + * PRINT handler types. + */ +export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void; +export type PrintFallbackHandlerType = PrintHandlerType; -export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; /** * EscapeSequenceParser interface. @@ -144,65 +163,82 @@ export interface IEscapeSequenceParser extends IDisposable { parse(data: Uint32Array, length: number): void; /** - * Get string from ident number. + * Get string from numercial function identifier `ident`. + * Useful in fallback handlers which expose the low level + * numcerical function identifier for debugging purposes. + * Note: A full back translation to `IFunctionIdentifier` + * is not implemented. */ identToString(ident: number): string; - setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; + setPrintHandler(handler: PrintHandlerType): void; clearPrintHandler(): void; - setExecuteHandler(flag: string, callback: () => void): void; - clearExecuteHandler(flag: string): void; - setExecuteHandlerFallback(callback: (code: number) => void): void; - - setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void; - clearCsiHandler(id: IFunctionIdentifier): void; - setCsiHandlerFallback(callback: (identifier: number, params: IParams) => void): void; - addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - - setEscHandler(id: IFunctionIdentifier, callback: () => void): void; + setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void; clearEscHandler(id: IFunctionIdentifier): void; - setEscHandlerFallback(callback: (identifier: number) => void): void; - addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable; + setEscHandlerFallback(handler: EscFallbackHandlerType): void; + addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable; - setOscHandler(ident: number, handler: IOscHandler): void; - clearOscHandler(ident: number): void; - setOscHandlerFallback(handler: OscFallbackHandler): void; - addOscHandler(ident: number, handler: IOscHandler): IDisposable; + setExecuteHandler(flag: string, handler: ExecuteHandlerType): void; + clearExecuteHandler(flag: string): void; + setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void; + + setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void; + clearCsiHandler(id: IFunctionIdentifier): void; + setCsiHandlerFallback(callback: CsiFallbackHandlerType): void; + addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable; setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; clearDcsHandler(id: IFunctionIdentifier): void; - setDcsHandlerFallback(handler: DcsFallbackHandler): void; + setDcsHandlerFallback(handler: DcsFallbackHandlerType): void; addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; - setErrorHandler(callback: (state: IParsingState) => IParsingState): void; + setOscHandler(ident: number, handler: IOscHandler): void; + clearOscHandler(ident: number): void; + setOscHandlerFallback(handler: OscFallbackHandlerType): void; + addOscHandler(ident: number, handler: IOscHandler): IDisposable; + + setErrorHandler(handler: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; } -export interface IOscParser extends IDisposable { - addOscHandler(ident: number, handler: IOscHandler): IDisposable; - setOscHandler(ident: number, handler: IOscHandler): void; - clearOscHandler(ident: number): void; - setOscHandlerFallback(handler: OscFallbackHandler): void; +/** + * Subparser interfaces. + * The subparsers are instantiated in `EscapeSequenceParser` and + * called during `EscapeSequenceParser.parse`. + */ +export interface ISubParser extends IDisposable { reset(): void; - start(): void; + addHandler(ident: number, handler: T): IDisposable; + setHandler(ident: number, handler: T): void; + clearHandler(ident: number): void; + setHandlerFallback(handler: U): void; put(data: Uint32Array, start: number, end: number): void; +} + +export interface IOscParser extends ISubParser { + start(): void; end(success: boolean): void; } -export interface IDcsParser extends IDisposable { - addDcsHandler(ident: number, handler: IDcsHandler): IDisposable; - setDcsHandler(ident: number, handler: IDcsHandler): void; - clearDcsHandler(ident: number): void; - setDcsHandlerFallback(handler: DcsFallbackHandler): void; - reset(): void; +export interface IDcsParser extends ISubParser { hook(ident: number, params: IParams): void; - put(data: Uint32Array, start: number, end: number): void; unhook(success: boolean): void; } +/** + * Interface to denote a specific ESC, CSI or DCS handler slot. + * The values are used to create an integer respresentation during handler + * regristation before passed to the subparsers as `ident`. + * The integer translation is made to allow a faster handler access + * in `EscapeSequenceParser.parse`. + */ export interface IFunctionIdentifier { prefix?: string; intermediates?: string; final: string; } + +export interface IHandlerCollection { + [key: string]: T[]; +} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 4ea65c10..33644e5d 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -11,11 +11,12 @@ import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; -import { IParams, IFunctionIdentifier } from 'common/parser/Types'; +import { IParams } from 'common/parser/Types'; export class Terminal implements ITerminalApi { private _core: ITerminal; private _addonManager: AddonManager; + private _parser: IParser; constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); @@ -33,6 +34,12 @@ export class Terminal implements ITerminalApi { public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } public get element(): HTMLElement { return this._core.element; } + public get parser(): IParser { + if (!this._parser) { + this._parser = new ParserApi(this._core); + } + return this._parser; + } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } @@ -57,18 +64,6 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { - return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); - } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); - } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { - return this._core.addEscHandler(id, handler); - } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._core.addOscHandler(ident, callback); - } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { return this._core.registerLinkMatcher(regex, handler, options); } @@ -170,9 +165,6 @@ export class Terminal implements ITerminalApi { public reset(): void { this._core.reset(); } - public static applyAddon(addon: any): void { - addon.apply(Terminal); - } public loadAddon(addon: ITerminalAddon): void { return this._addonManager.loadAddon(this, addon); } @@ -226,3 +218,20 @@ class BufferCellApiView implements IBufferCellApi { public get char(): string { return this._line.getString(this._x); } public get width(): number { return this._line.getWidth(this._x); } } + +class ParserApi implements IParser { + constructor(private _core: ITerminal) {} + + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); + } + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); + } + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + return this._core.addEscHandler(id, handler); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._core.addOscHandler(ident, callback); + } +} diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 22b50349..d2c80235 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -165,7 +165,13 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Blink animation styles += - `@keyframes blink {` + + `@keyframes blink_box_shadow {` + + ` 50% {` + + ` box-shadow: none;` + + ` }` + + `}`; + styles += + `@keyframes blink_block {` + ` 0% {` + ` background-color: ${this._colors.cursor.css};` + ` color: ${this._colors.cursorAccent.css};` + @@ -181,8 +187,11 @@ export class DomRenderer extends Disposable implements IRenderer { ` outline: 1px solid ${this._colors.cursor.css};` + ` outline-offset: -1px;` + `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` + - ` animation: blink 1s step-end infinite;` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` + + ` animation: blink_box_shadow 1s step-end infinite;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + + ` animation: blink_block 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${this._colors.cursor.css};` + diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 4b784642..9ca0c943 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -334,95 +334,6 @@ describe('InputHandler Integration Tests', function(): void { }); }); }); - - describe('addCsiHandler', () => { - it('should call custom CSI handler with js array params', async () => { - await page.evaluate(` - window.term.reset(); - const _customCsiHandlerParams = []; - const _customCsiHandler = window.term.addCsiHandler({final: 'm'}, (params, collect) => { - _customCsiHandlerParams.push(params); - return false; - }, ''); - `); - await page.evaluate(` - window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - `); - assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); - }); - }); - describe('addDcsHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - const _customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - _customDcsHandlerCallStack.push(['A', params, data]); - return false; - }); - const _customDcsHandlerB = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - _customDcsHandlerCallStack.push(['B', params, data]); - return true; - }); - const _customDcsHandlerC = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - _customDcsHandlerCallStack.push(['C', params, data]); - return false; - }); - `); - await page.evaluate(` - window.term.write('\x1bP1;2+psome data\x1b\\\\'); - `); - assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); - }); - }); - describe('addEscHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - const _customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { - _customEscHandlerCallStack.push('A'); - return false; - }); - const _customEscHandlerB = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { - _customEscHandlerCallStack.push('B'); - return true; - }); - const _customEscHandlerC = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { - _customEscHandlerCallStack.push('C'); - return false; - }); - `); - await page.evaluate(` - window.term.write('\x1b(B'); - `); - assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']); - }); - }); - describe('addOscHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - const _customOscHandlerCallStack = []; - const _customOscHandlerA = window.term.addOscHandler(1234, data => { - _customOscHandlerCallStack.push(['A', data]); - return false; - }); - const _customOscHandlerB = window.term.addOscHandler(1234, data => { - _customOscHandlerCallStack.push(['B', data]); - return true; - }); - const _customOscHandlerC = window.term.addOscHandler(1234, data => { - _customOscHandlerCallStack.push(['C', data]); - return false; - }); - `); - await page.evaluate(` - window.term.write('\x1b]1234;some data\x07'); - `); - assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); - }); - }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts new file mode 100644 index 00000000..28f7d8b3 --- /dev/null +++ b/test/api/Parser.api.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('Parser Integration Tests', function(): void { + this.timeout(20000); + + before(async function(): Promise { + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + await page.goto(APP); + await openTerminal(); + }); + + after(() => { + browser.close(); + }); + + describe('addCsiHandler', () => { + it('should call custom CSI handler with js array params', async () => { + await page.evaluate(` + window.term.reset(); + const _customCsiHandlerParams = []; + const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => { + _customCsiHandlerParams.push(params); + return false; + }, ''); + `); + await page.evaluate(` + window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); + `); + assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); + }); + }); + describe('addDcsHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customDcsHandlerCallStack = []; + const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + _customDcsHandlerCallStack.push(['A', params, data]); + return false; + }); + const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + _customDcsHandlerCallStack.push(['B', params, data]); + return true; + }); + const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + _customDcsHandlerCallStack.push(['C', params, data]); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1bP1;2+psome data\x1b\\\\'); + `); + assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); + }); + }); + describe('addEscHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customEscHandlerCallStack = []; + const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + _customEscHandlerCallStack.push('A'); + return false; + }); + const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + _customEscHandlerCallStack.push('B'); + return true; + }); + const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + _customEscHandlerCallStack.push('C'); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1b(B'); + `); + assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']); + }); + }); + describe('addOscHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customOscHandlerCallStack = []; + const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['A', data]); + return false; + }); + const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['B', data]); + return true; + }); + const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['C', data]); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1b]1234;some data\x07'); + `); + assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); + }); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index a8b9fde9..fa22dc58 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -7,7 +7,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { C0, C1 } from 'common/data/EscapeSequences'; import { IDcsHandler, IParams } from 'common/parser/Types'; -import { OscHandlerFactory } from 'common/parser/OscParser'; +import { OscHandler } from 'common/parser/OscParser'; function toUtf32(s: string): Uint32Array { @@ -80,8 +80,8 @@ perfContext('Parser throughput - 50MB data', () => { parser.setExecuteHandler(C1.IND, () => {}); parser.setExecuteHandler(C1.NEL, () => {}); parser.setExecuteHandler(C1.HTS, () => {}); - parser.setOscHandler(0, new OscHandlerFactory((data) => {})); - parser.setOscHandler(2, new OscHandlerFactory((data) => {})); + parser.setOscHandler(0, new OscHandler((data) => {})); + parser.setOscHandler(2, new OscHandler((data) => {})); parser.setEscHandler({final: '7'}, () => {}); parser.setEscHandler({final: '8'}, () => {}); parser.setEscHandler({final: 'D'}, () => {}); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3891004e..4ec6fc1c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -207,47 +207,47 @@ declare module 'xterm' { */ export interface ITheme { /** The default foreground color */ - foreground?: string, + foreground?: string; /** The default background color */ - background?: string, + background?: string; /** The cursor color */ - cursor?: string, + cursor?: string; /** The accent color of the cursor (fg color for a block cursor) */ - cursorAccent?: string, + cursorAccent?: string; /** The selection background color (can be transparent) */ - selection?: string, + selection?: string; /** ANSI black (eg. `\x1b[30m`) */ - black?: string, + black?: string; /** ANSI red (eg. `\x1b[31m`) */ - red?: string, + red?: string; /** ANSI green (eg. `\x1b[32m`) */ - green?: string, + green?: string; /** ANSI yellow (eg. `\x1b[33m`) */ - yellow?: string, + yellow?: string; /** ANSI blue (eg. `\x1b[34m`) */ - blue?: string, + blue?: string; /** ANSI magenta (eg. `\x1b[35m`) */ - magenta?: string, + magenta?: string; /** ANSI cyan (eg. `\x1b[36m`) */ - cyan?: string, + cyan?: string; /** ANSI white (eg. `\x1b[37m`) */ - white?: string, + white?: string; /** ANSI bright black (eg. `\x1b[1;30m`) */ - brightBlack?: string, + brightBlack?: string; /** ANSI bright red (eg. `\x1b[1;31m`) */ - brightRed?: string, + brightRed?: string; /** ANSI bright green (eg. `\x1b[1;32m`) */ - brightGreen?: string, + brightGreen?: string; /** ANSI bright yellow (eg. `\x1b[1;33m`) */ - brightYellow?: string, + brightYellow?: string; /** ANSI bright blue (eg. `\x1b[1;34m`) */ - brightBlue?: string, + brightBlue?: string; /** ANSI bright magenta (eg. `\x1b[1;35m`) */ - brightMagenta?: string, + brightMagenta?: string; /** ANSI bright cyan (eg. `\x1b[1;36m`) */ - brightCyan?: string, + brightCyan?: string; /** ANSI bright white (eg. `\x1b[1;37m`) */ - brightWhite?: string + brightWhite?: string; } /** @@ -386,6 +386,12 @@ declare module 'xterm' { */ readonly markers: ReadonlyArray; + /** + * (EXPERIMENTAL) Get the parser interface to register + * custom escape sequence handlers. + */ + readonly parser: IParser; + /** * Natural language strings that can be localized. */ @@ -500,70 +506,6 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - /** - * Adds a handler for CSI escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {final: 'm'} for SGR. - * @param callback The function to handle the sequence. The callback is - * called with the numerical params. If the sequence has subparams the - * array will contain subarrays with their numercial values. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; - - /** - * Adds a handler for DCS escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. - * @param callback The function to handle the sequence. Note that the - * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since DCS sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits DCS payload to 10 MB - * which should give enough room for most use cases. - * The function gets the payload and numerical parameters as arguments. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addDcsHandler or setDcsHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; - - /** - * Adds a handler for ESC escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '%' final: 'G'} for - * default charset selection. - * @param callback The function to handle the sequence. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addEscHandler or setEscHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; - - /** - * Adds a handler for OSC escape sequences. - * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the sequence. Note that the - * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since OSC sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits OSC payload to 10 MB - * which should give enough room for most use cases. - * The callback is called with OSC data string. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addOscHandler or setOscHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; - /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. @@ -842,18 +784,10 @@ declare module 'xterm' { /** * Perform a full reset (RIS, aka '\x1bc'). */ - reset(): void + reset(): void; /** - * Applies an addon to the Terminal prototype, making it available to all - * newly created Terminals. - * @param addon The addon to apply. - * @deprecated Use the new loadAddon API/addon format. - */ - static applyAddon(addon: any): void; - - /** - * (EXPERIMENTAL) Loads an addon into this instance of xterm.js. + * Loads an addon into this instance of xterm.js. * @param addon The addon to load. */ loadAddon(addon: ITerminalAddon): void; @@ -991,17 +925,18 @@ declare module 'xterm' { } /** - * Data type to register a CSI, DCS or ESC callback in the parser in the form: + * (EXPERIMENTAL) Data type to register a CSI, DCS or ESC callback in the parser + * in the form: * ESC I..I F * CSI Prefix P..P I..I F * DCS Prefix P..P I..I F data_bytes ST - * + * * with these rules/restrictions: * - prefix can only be used with CSI and DCS * - only one leading prefix byte is recognized by the parser * before any other parameter bytes (P..P) * - intermediate bytes are recognized up to 2 - * + * * For custom sequences make sure to read ECMA-48 and the resources at * vt100.net to not clash with existing sequences or reserved address space. * General recommendations: @@ -1011,9 +946,9 @@ declare module 'xterm' { * thus parsers might get confused with more intermediates) * - test against other common emulators to check whether they escape/ignore * the sequence correctly - * + * * Notes: OSC command registration is handled differently (see addOscHandler) - * APC, PM or SOS is currently not supported + * APC, PM or SOS is currently not supported. */ export interface IFunctionIdentifier { /** @@ -1032,4 +967,73 @@ declare module 'xterm' { */ final: string; } + + /** + * (EXPERIMENTAL) Parser interface. + */ + export interface IParser { + /** + * Adds a handler for CSI escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. If the sequence has subparams the + * array will contain subarrays with their numercial values. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for DCS escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for ESC escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + + /** + * Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addOscHandler or setOscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + } }