From 20fa44703310cc38237afbe515a0beed68565e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 8 Aug 2019 19:34:00 +0200 Subject: [PATCH] namespace parser related stuff --- src/public/Terminal.ts | 36 +++--- test/api/InputHandler.api.ts | 89 --------------- test/api/Parser.api.ts | 134 ++++++++++++++++++++++ typings/xterm.d.ts | 209 ++++++++++++++++++----------------- 4 files changed, 266 insertions(+), 202 deletions(-) create mode 100644 test/api/Parser.api.ts diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 4ea65c10..0a91aa74 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, Parser } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -11,7 +11,9 @@ 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, IEscapeSequenceParser } from 'common/parser/Types'; +import { OscHandlerFactory } from 'common/parser/OscParser'; +import { DcsHandlerFactory } from '../../out/common/parser/DcsParser'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -33,6 +35,7 @@ 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(): Parser.IParser { return new ParserApi((this._core as any)._inputHandler._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 +60,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); } @@ -226,3 +217,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 Parser.IParser { + constructor(private _parser: IEscapeSequenceParser) {} + + public addCsiHandler(id: Parser.IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this._parser.addCsiHandler(id, (params: IParams) => callback(params.toArray())); + } + public addDcsHandler(id: Parser.IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this._parser.addDcsHandler(id, new DcsHandlerFactory((data: string, params: IParams) => callback(data, params.toArray()))); + } + public addEscHandler(id: Parser.IFunctionIdentifier, handler: () => boolean): IDisposable { + return this._parser.addEscHandler(id, handler); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._parser.addOscHandler(ident, new OscHandlerFactory(callback)); + } +} 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/typings/xterm.d.ts b/typings/xterm.d.ts index 3891004e..de767f59 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -500,70 +500,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. @@ -991,45 +927,120 @@ declare module 'xterm' { } /** - * 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: - * - use private address space (see ECMA-48) - * - use max one intermediate byte (technically not limited by the spec, - * in practice there are no sequences with more than one intermediate byte, - * 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 + * Parser namespace, contains all parser related bits. */ - export interface IFunctionIdentifier { + export namespace Parser { + /** - * Optional prefix byte, must be in range \x3c .. \x3f. - * Usable in CSI and DCS. + * 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: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * 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. */ - prefix?: string; + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. + */ + final: string; + } + /** - * Optional intermediate bytes, must be in range \x20 .. \x2f. - * Usable in CSI, DCS and ESC. + * Parser interface. */ - intermediates?: string; - /** - * Final byte, must be in range \x40 .. \x7e for CSI and DCS, - * \x30 .. \x7e for ESC. - */ - final: string; + 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; + } } }