diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 84740e1e7..79b350e17 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -7,6 +7,7 @@ }, "main": "lib/xterm-addon-attach.js", "types": "typings/xterm-addon-attach.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p src", diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index 6e5537003..77471c261 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -7,6 +7,7 @@ }, "main": "lib/xterm-addon-fit.js", "types": "typings/xterm-addon-fit.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p src", diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 5a607fe9d..6871c5662 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -7,6 +7,7 @@ }, "main": "lib/xterm-addon-search.js", "types": "typings/xterm-addon-search.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p src", diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index e9f8a9b40..4d3e8841e 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/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index fd3945d32..2e3906900 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -7,6 +7,7 @@ }, "main": "lib/xterm-addon-web-links.js", "types": "typings/xterm-addon-web-links.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p src", diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index e4f341476..0db5b2042 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -7,6 +7,7 @@ }, "main": "lib/xterm-addon-webgl.js", "types": "typings/xterm-addon-webgl.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p src", diff --git a/demo/start.js b/demo/start.js index a054e939c..a14627c1d 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/src/InputHandler.test.ts b/src/InputHandler.test.ts index 38645a0bd..fbbafc110 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test'; import { IBufferService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -33,7 +33,7 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); @@ -52,40 +52,39 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); - const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService); - const collect = ' '; + const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService()); - inputHandler.setCursorStyle(Params.fromArray([0]), collect); + inputHandler.setCursorStyle(Params.fromArray([0])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([1]), collect); + inputHandler.setCursorStyle(Params.fromArray([1])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([2]), collect); + inputHandler.setCursorStyle(Params.fromArray([2])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], false); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([3]), collect); + inputHandler.setCursorStyle(Params.fromArray([3])); assert.equal(optionsService.options['cursorStyle'], 'underline'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([4]), collect); + inputHandler.setCursorStyle(Params.fromArray([4])); assert.equal(optionsService.options['cursorStyle'], 'underline'); assert.equal(optionsService.options['cursorBlink'], false); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([5]), collect); + inputHandler.setCursorStyle(Params.fromArray([5])); assert.equal(optionsService.options['cursorStyle'], 'bar'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([6]), collect); + inputHandler.setCursorStyle(Params.fromArray([6])); assert.equal(optionsService.options['cursorStyle'], 'bar'); assert.equal(optionsService.options['cursorBlink'], false); }); @@ -93,14 +92,13 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle Terminal.bracketedPasteMode', () => { const terminal = new MockInputHandlingTerminal(); - const collect = '?'; terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Set bracketed paste mode - inputHandler.setMode(Params.fromArray([2004]), collect); + inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, true); // Reset bracketed paste mode - inputHandler.resetMode(Params.fromArray([2004]), collect); + inputHandler.resetModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, false); }); }); @@ -114,7 +112,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -152,7 +150,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -193,7 +191,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -222,7 +220,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -357,7 +355,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -372,7 +370,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); + handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 381671e0f..56b2d8be1 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,13 +14,14 @@ import { concat } from 'common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; -import { ISelectionService } from 'browser/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; +import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from 'common/parser/DcsParser'; /** * Map collect to glevel. Used in `selectCharset`. @@ -48,7 +49,7 @@ class DECRQSS implements IDcsHandler { private _optionsService: IOptionsService ) { } - hook(collect: string, params: IParams, flag: number): void { + hook(params: IParams): void { this._data = new Uint32Array(0); } @@ -56,7 +57,11 @@ class DECRQSS implements IDcsHandler { this._data = concat(this._data, data.subarray(start, end)); } - unhook(): void { + unhook(success: boolean): void { + if (!success) { + this._data = new Uint32Array(0); + return; + } const data = utf32ToString(this._data); this._data = new Uint32Array(0); switch (data) { @@ -118,8 +123,6 @@ export class InputHandler extends Disposable implements IInputHandler { private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); - private _selectionService: ISelectionService | undefined; - private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); @@ -134,6 +137,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, private readonly _optionsService: IOptionsService, + private readonly _coreMouseService: ICoreMouseService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { super(); @@ -143,63 +147,75 @@ export class InputHandler extends Disposable implements IInputHandler { /** * custom fallback handlers */ - this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - this._logService.debug('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) }); + this._parser.setCsiHandlerFallback((ident, params) => { + this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() }); }); - this._parser.setEscHandlerFallback((collect: string, flag: number) => { - this._logService.debug('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) }); + this._parser.setEscHandlerFallback(ident => { + this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) }); }); - this._parser.setExecuteHandlerFallback((code: number) => { + this._parser.setExecuteHandlerFallback(code => { this._logService.debug('Unknown EXECUTE code: ', { code }); }); - this._parser.setOscHandlerFallback((identifier: number, data: string) => { - this._logService.debug('Unknown OSC code: ', { identifier, data }); + this._parser.setOscHandlerFallback((identifier, action, data) => { + this._logService.debug('Unknown OSC code: ', { identifier, action, data }); + }); + this._parser.setDcsHandlerFallback((ident, action, payload) => { + if (action === 'HOOK') { + payload = payload.toArray(); + } + this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload }); }); /** * print handler */ - this._parser.setPrintHandler((data, start, end): void => this.print(data, start, end)); + this._parser.setPrintHandler((data, start, end) => this.print(data, start, end)); /** * CSI handler */ - this._parser.setCsiHandler('@', (params, collect) => this.insertChars(params)); - this._parser.setCsiHandler('A', (params, collect) => this.cursorUp(params)); - this._parser.setCsiHandler('B', (params, collect) => this.cursorDown(params)); - this._parser.setCsiHandler('C', (params, collect) => this.cursorForward(params)); - this._parser.setCsiHandler('D', (params, collect) => this.cursorBackward(params)); - this._parser.setCsiHandler('E', (params, collect) => this.cursorNextLine(params)); - this._parser.setCsiHandler('F', (params, collect) => this.cursorPrecedingLine(params)); - this._parser.setCsiHandler('G', (params, collect) => this.cursorCharAbsolute(params)); - this._parser.setCsiHandler('H', (params, collect) => this.cursorPosition(params)); - this._parser.setCsiHandler('I', (params, collect) => this.cursorForwardTab(params)); - this._parser.setCsiHandler('J', (params, collect) => this.eraseInDisplay(params)); - this._parser.setCsiHandler('K', (params, collect) => this.eraseInLine(params)); - this._parser.setCsiHandler('L', (params, collect) => this.insertLines(params)); - this._parser.setCsiHandler('M', (params, collect) => this.deleteLines(params)); - this._parser.setCsiHandler('P', (params, collect) => this.deleteChars(params)); - this._parser.setCsiHandler('S', (params, collect) => this.scrollUp(params)); - this._parser.setCsiHandler('T', (params, collect) => this.scrollDown(params, collect)); - this._parser.setCsiHandler('X', (params, collect) => this.eraseChars(params)); - this._parser.setCsiHandler('Z', (params, collect) => this.cursorBackwardTab(params)); - this._parser.setCsiHandler('`', (params, collect) => this.charPosAbsolute(params)); - this._parser.setCsiHandler('a', (params, collect) => this.hPositionRelative(params)); - this._parser.setCsiHandler('b', (params, collect) => this.repeatPrecedingCharacter(params)); - this._parser.setCsiHandler('c', (params, collect) => this.sendDeviceAttributes(params, collect)); - this._parser.setCsiHandler('d', (params, collect) => this.linePosAbsolute(params)); - this._parser.setCsiHandler('e', (params, collect) => this.vPositionRelative(params)); - this._parser.setCsiHandler('f', (params, collect) => this.hVPosition(params)); - this._parser.setCsiHandler('g', (params, collect) => this.tabClear(params)); - this._parser.setCsiHandler('h', (params, collect) => this.setMode(params, collect)); - this._parser.setCsiHandler('l', (params, collect) => this.resetMode(params, collect)); - this._parser.setCsiHandler('m', (params, collect) => this.charAttributes(params)); - this._parser.setCsiHandler('n', (params, collect) => this.deviceStatus(params, collect)); - this._parser.setCsiHandler('p', (params, collect) => this.softReset(params, collect)); - this._parser.setCsiHandler('q', (params, collect) => this.setCursorStyle(params, collect)); - this._parser.setCsiHandler('r', (params, collect) => this.setScrollRegion(params, collect)); - this._parser.setCsiHandler('s', (params, collect) => this.saveCursor(params)); - this._parser.setCsiHandler('u', (params, collect) => this.restoreCursor(params)); + this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params)); + this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params)); + this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params)); + this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params)); + this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params)); + this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params)); + this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params)); + this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params)); + this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params)); + this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params)); + this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params)); + this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params)); + this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params)); + this._parser.setCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params)); + this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params)); + this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params)); + this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params)); + this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params)); + this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params)); + this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params)); + this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params)); + this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params)); + this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params)); + this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params)); + this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params)); + this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params)); + this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params)); + this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params)); + this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params)); + this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params)); + this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params)); + this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params)); + this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params)); + this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params)); + this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params)); + this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params)); + this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params)); + this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params)); + this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params)); + this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params)); + this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); + this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); /** * execute handler @@ -215,7 +231,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); // FIXME: What do to with missing? Old code just added those to print. - // some C1 control codes - FIXME: should those be enabled by default? this._parser.setExecuteHandler(C1.IND, () => this.index()); this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); @@ -224,10 +239,10 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC handler */ // 0 - icon name + title - this._parser.setOscHandler(0, (data) => this.setTitle(data)); + this._parser.setOscHandler(0, new OscHandler((data: string) => this.setTitle(data))); // 1 - icon name // 2 - title - this._parser.setOscHandler(2, (data) => 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 @@ -264,32 +279,32 @@ export class InputHandler extends Disposable implements IInputHandler { /** * ESC handlers */ - this._parser.setEscHandler('7', () => this.saveCursor()); - this._parser.setEscHandler('8', () => this.restoreCursor()); - this._parser.setEscHandler('D', () => this.index()); - this._parser.setEscHandler('E', () => this.nextLine()); - this._parser.setEscHandler('H', () => this.tabSet()); - this._parser.setEscHandler('M', () => this.reverseIndex()); - this._parser.setEscHandler('=', () => this.keypadApplicationMode()); - this._parser.setEscHandler('>', () => this.keypadNumericMode()); - this._parser.setEscHandler('c', () => this.reset()); - this._parser.setEscHandler('n', () => this.setgLevel(2)); - this._parser.setEscHandler('o', () => this.setgLevel(3)); - this._parser.setEscHandler('|', () => this.setgLevel(3)); - this._parser.setEscHandler('}', () => this.setgLevel(2)); - this._parser.setEscHandler('~', () => this.setgLevel(1)); - this._parser.setEscHandler('%@', () => this.selectDefaultCharset()); - this._parser.setEscHandler('%G', () => this.selectDefaultCharset()); + this._parser.setEscHandler({final: '7'}, () => this.saveCursor()); + this._parser.setEscHandler({final: '8'}, () => this.restoreCursor()); + this._parser.setEscHandler({final: 'D'}, () => this.index()); + this._parser.setEscHandler({final: 'E'}, () => this.nextLine()); + this._parser.setEscHandler({final: 'H'}, () => this.tabSet()); + this._parser.setEscHandler({final: 'M'}, () => this.reverseIndex()); + this._parser.setEscHandler({final: '='}, () => this.keypadApplicationMode()); + this._parser.setEscHandler({final: '>'}, () => this.keypadNumericMode()); + this._parser.setEscHandler({final: 'c'}, () => this.reset()); + this._parser.setEscHandler({final: 'n'}, () => this.setgLevel(2)); + this._parser.setEscHandler({final: 'o'}, () => this.setgLevel(3)); + this._parser.setEscHandler({final: '|'}, () => this.setgLevel(3)); + this._parser.setEscHandler({final: '}'}, () => this.setgLevel(2)); + this._parser.setEscHandler({final: '~'}, () => this.setgLevel(1)); + this._parser.setEscHandler({intermediates: '%', final: '@'}, () => this.selectDefaultCharset()); + this._parser.setEscHandler({intermediates: '%', final: 'G'}, () => this.selectDefaultCharset()); for (const flag in CHARSETS) { - this._parser.setEscHandler('(' + flag, () => this.selectCharset('(' + flag)); - this._parser.setEscHandler(')' + flag, () => this.selectCharset(')' + flag)); - this._parser.setEscHandler('*' + flag, () => this.selectCharset('*' + flag)); - this._parser.setEscHandler('+' + flag, () => this.selectCharset('+' + flag)); - this._parser.setEscHandler('-' + flag, () => this.selectCharset('-' + flag)); - this._parser.setEscHandler('.' + flag, () => this.selectCharset('.' + flag)); - this._parser.setEscHandler('/' + flag, () => this.selectCharset('/' + flag)); // TODO: supported? + this._parser.setEscHandler({intermediates: '(', final: flag}, () => this.selectCharset('(' + flag)); + this._parser.setEscHandler({intermediates: ')', final: flag}, () => this.selectCharset(')' + flag)); + this._parser.setEscHandler({intermediates: '*', final: flag}, () => this.selectCharset('*' + flag)); + this._parser.setEscHandler({intermediates: '+', final: flag}, () => this.selectCharset('+' + flag)); + this._parser.setEscHandler({intermediates: '-', final: flag}, () => this.selectCharset('-' + flag)); + this._parser.setEscHandler({intermediates: '.', final: flag}, () => this.selectCharset('.' + flag)); + this._parser.setEscHandler({intermediates: '/', final: flag}, () => this.selectCharset('/' + flag)); // TODO: supported? } - this._parser.setEscHandler('#8', () => this.screenAlignmentPattern()); + this._parser.setEscHandler({intermediates: '#', final: '8'}, () => this.screenAlignmentPattern()); /** * error handler @@ -302,18 +317,13 @@ export class InputHandler extends Disposable implements IInputHandler { /** * DCS handler */ - this._parser.setDcsHandler('$q', new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); + this._parser.setDcsHandler({intermediates: '$', final: 'q'}, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); } public dispose(): void { super.dispose(); } - // TODO: When InputHandler moves into common, browser dependencies need to move out - public setBrowserServices(selectionService: ISelectionService): void { - this._selectionService = selectionService; - } - public parse(data: string): void { let buffer = this._bufferService.buffer; const cursorStartX = buffer.x; @@ -480,15 +490,29 @@ export class InputHandler extends Disposable implements IInputHandler { /** * Forward addCsiHandler from parser. */ - public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - return this._parser.addCsiHandler(flag, callback); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._parser.addCsiHandler(id, callback); + } + + /** + * Forward addDcsHandler from parser. + */ + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._parser.addDcsHandler(id, new DcsHandler(callback)); + } + + /** + * Forward addEscHandler from parser. + */ + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._parser.addEscHandler(id, callback); } /** * Forward addOscHandler from parser. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, callback); + return this._parser.addOscHandler(ident, new OscHandler(callback)); } /** @@ -1021,8 +1045,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: IParams, collect?: string): void { - if (params.length < 2 && !collect) { + public scrollDown(params: IParams): void { + if (params.length < 2) { let param = params.params[0] || 1; // make buffer local for faster access @@ -1125,32 +1149,33 @@ export class InputHandler extends Disposable implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: IParams, collect?: string): void { + public sendDeviceAttributesPrimary(params: IParams): void { if (params.params[0] > 0) { return; } - - if (!collect) { - if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { - this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); - } else if (this._terminal.is('linux')) { - this._coreService.triggerDataEvent(C0.ESC + '[?6c'); - } - } else if (collect === '>') { - // xterm and urxvt - // seem to spit this - // out around ~370 times (?). - if (this._terminal.is('xterm')) { - this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); - } else if (this._terminal.is('rxvt-unicode')) { - this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); - } else if (this._terminal.is('linux')) { - // not supported by linux console. - // linux console echoes parameters. - this._coreService.triggerDataEvent(params.params[0] + 'c'); - } else if (this._terminal.is('screen')) { - this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); - } + if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); + } else if (this._terminal.is('linux')) { + this._coreService.triggerDataEvent(C0.ESC + '[?6c'); + } + } + public sendDeviceAttributesSecondary(params: IParams): void { + if (params.params[0] > 0) { + return; + } + // xterm and urxvt + // seem to spit this + // out around ~370 times (?). + if (this._terminal.is('xterm')) { + this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); + } else if (this._terminal.is('rxvt-unicode')) { + this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); + } else if (this._terminal.is('linux')) { + // not supported by linux console. + // linux console echoes parameters. + this._coreService.triggerDataEvent(params.params[0] + 'c'); + } else if (this._terminal.is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } } @@ -1240,15 +1265,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: IParams, collect?: string): void { + public setMode(params: IParams): void { for (let i = 0; i < params.length; i++) { - this._setMode(params.params[i], collect); - } - } - - private _setMode(param: number, collect?: string): void { - if (!collect) { - switch (param) { + switch (params.params[i]) { case 4: this._terminal.insertMode = true; break; @@ -1256,8 +1275,11 @@ export class InputHandler extends Disposable implements IInputHandler { // this._t.convertEol = true; break; } - } else if (collect === '?') { - switch (param) { + } + } + public setModePrivate(params: IParams): void { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = true; break; @@ -1293,27 +1315,19 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. + this._coreMouseService.activeProtocol = 'X10'; + break; case 1000: // vt200 mouse // no motion. - // no modifiers, except control on the wheel. + this._coreMouseService.activeProtocol = 'VT200'; + break; case 1002: // button event mouse + this._coreMouseService.activeProtocol = 'DRAG'; + break; case 1003: // any event mouse // any event - sends motion events, // even if there is no button held down. - - // TODO: Why are params[0] compares nested within a switch for params[0]? - - this._terminal.x10Mouse = param === 9; - this._terminal.vt200Mouse = param === 1000; - this._terminal.normalMouse = param > 1000; - this._terminal.mouseEvents = true; - if (this._terminal.element) { - this._terminal.element.classList.add('enable-mouse-events'); - } - if (this._selectionService) { - this._selectionService.disable(); - } - this._logService.debug('Binding to mouse events.'); + this._coreMouseService.activeProtocol = 'ANY'; break; case 1004: // send focusin/focusout events // focusin: ^[[I @@ -1321,23 +1335,15 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.sendFocus = true; break; case 1005: // utf8 ext mode mouse - this._terminal.utfMouse = true; // for wide terminals // simply encodes large values as utf8 characters + this._coreMouseService.activeEncoding = 'UTF8'; break; case 1006: // sgr ext mode mouse - this._terminal.sgrMouse = true; - // for wide terminals - // does not add 32 to fields - // press: ^[[ Keyboard Action Mode (AM). @@ -1446,15 +1453,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: IParams, collect?: string): void { + public resetMode(params: IParams): void { for (let i = 0; i < params.length; i++) { - this._resetMode(params.params[i], collect); - } - } - - private _resetMode(param: number, collect?: string): void { - if (!collect) { - switch (param) { + switch (params.params[i]) { case 4: this._terminal.insertMode = false; break; @@ -1462,8 +1463,11 @@ export class InputHandler extends Disposable implements IInputHandler { // this._t.convertEol = false; break; } - } else if (collect === '?') { - switch (param) { + } + } + public resetModePrivate(params: IParams): void { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = false; break; @@ -1498,28 +1502,19 @@ export class InputHandler extends Disposable implements IInputHandler { case 1000: // vt200 mouse case 1002: // button event mouse case 1003: // any event mouse - this._terminal.x10Mouse = false; - this._terminal.vt200Mouse = false; - this._terminal.normalMouse = false; - this._terminal.mouseEvents = false; - if (this._terminal.element) { - this._terminal.element.classList.remove('enable-mouse-events'); - } - if (this._selectionService) { - this._selectionService.enable(); - } + this._coreMouseService.activeProtocol = 'NONE'; break; case 1004: // send focusin/focusout events this._terminal.sendFocus = false; break; case 1005: // utf8 ext mode mouse - this._terminal.utfMouse = false; + this._coreMouseService.activeEncoding = 'DEFAULT'; break; case 1006: // sgr ext mode mouse - this._terminal.sgrMouse = false; + this._coreMouseService.activeEncoding = 'DEFAULT'; break; case 1015: // urxvt ext mode mouse - this._terminal.urxvtMouse = false; + this._coreMouseService.activeEncoding = 'DEFAULT'; break; case 25: // hide cursor this._terminal.cursorHidden = true; @@ -1533,7 +1528,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 1047: // normal screen buffer - clearing it first // Ensure the selection manager has the correct buffer this._bufferService.buffers.activateNormalBuffer(); - if (param === 1049) { + if (params.params[i] === 1049) { this.restoreCursor(); } this._terminal.refresh(0, this._bufferService.rows - 1); @@ -1807,47 +1802,47 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: IParams, collect?: string): void { - if (!collect) { - switch (params.params[0]) { - case 5: - // status report - this._coreService.triggerDataEvent(`${C0.ESC}[0n`); - break; - case 6: - // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; - this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); - break; - } - } else if (collect === '?') { - // modern xterm doesnt seem to - // respond to any of these except ?6, 6, and 5 - switch (params.params[0]) { - case 6: - // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; - this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); - break; - case 15: - // no printer - // this.handler(C0.ESC + '[?11n'); - break; - case 25: - // dont support user defined keys - // this.handler(C0.ESC + '[?21n'); - break; - case 26: - // north american keyboard - // this.handler(C0.ESC + '[?27;1;0;0n'); - break; - case 53: - // no dec locator/mouse - // this.handler(C0.ESC + '[?50n'); - break; - } + public deviceStatus(params: IParams): void { + switch (params.params[0]) { + case 5: + // status report + this._coreService.triggerDataEvent(`${C0.ESC}[0n`); + break; + case 6: + // cursor position + const y = this._bufferService.buffer.y + 1; + const x = this._bufferService.buffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); + break; + } + } + + public deviceStatusPrivate(params: IParams): void { + // modern xterm doesnt seem to + // respond to any of these except ?6, 6, and 5 + switch (params.params[0]) { + case 6: + // cursor position + const y = this._bufferService.buffer.y + 1; + const x = this._bufferService.buffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); + break; + case 15: + // no printer + // this.handler(C0.ESC + '[?11n'); + break; + case 25: + // dont support user defined keys + // this.handler(C0.ESC + '[?21n'); + break; + case 26: + // north american keyboard + // this.handler(C0.ESC + '[?27;1;0;0n'); + break; + case 53: + // no dec locator/mouse + // this.handler(C0.ESC + '[?50n'); + break; } } @@ -1855,25 +1850,23 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: IParams, collect?: string): void { - if (collect === '!') { - this._terminal.cursorHidden = false; - this._terminal.insertMode = false; - this._terminal.originMode = false; - this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false - this._terminal.applicationKeypad = false; // ? - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } - this._coreService.decPrivateModes.applicationCursorKeys = false; - this._bufferService.buffer.scrollTop = 0; - this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; - this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); - this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + public softReset(params: IParams): void { + this._terminal.cursorHidden = false; + this._terminal.insertMode = false; + this._terminal.originMode = false; + this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false + this._terminal.applicationKeypad = false; // ? + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); } + this._coreService.decPrivateModes.applicationCursorKeys = false; + this._bufferService.buffer.scrollTop = 0; + this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; + this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? + this._terminal.charset = null; + this._terminal.glevel = 0; // ?? + this._terminal.charsets = [null]; // ?? } /** @@ -1886,40 +1879,32 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: IParams, collect?: string): void { - if (collect === ' ') { - const param = params.params[0] || 1; - switch (param) { - case 1: - case 2: - this._optionsService.options.cursorStyle = 'block'; - break; - case 3: - case 4: - this._optionsService.options.cursorStyle = 'underline'; - break; - case 5: - case 6: - this._optionsService.options.cursorStyle = 'bar'; - break; - } - const isBlinking = param % 2 === 1; - this._optionsService.options.cursorBlink = isBlinking; + public setCursorStyle(params: IParams): void { + const param = params.params[0] || 1; + switch (param) { + case 1: + case 2: + this._optionsService.options.cursorStyle = 'block'; + break; + case 3: + case 4: + this._optionsService.options.cursorStyle = 'underline'; + break; + case 5: + case 6: + this._optionsService.options.cursorStyle = 'bar'; + break; } + const isBlinking = param % 2 === 1; + this._optionsService.options.cursorBlink = isBlinking; } /** * CSI Ps ; Ps r * Set Scrolling Region [top;bottom] (default = full size of win- * dow) (DECSTBM). - * CSI ? Pm r - * currently skipped */ - public setScrollRegion(params: IParams, collect?: string): void { - if (collect) { - return; - } - + public setScrollRegion(params: IParams): void { const top = params.params[0] || 1; let bottom: number; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 714257f9d..59864f0a3 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -224,6 +224,31 @@ describe('Terminal', () => { }); }); + describe('paste', () => { + it('should fire data event', done => { + term.onData(e => { + assert.equal(e, 'foo'); + done(); + }); + term.paste('foo'); + }); + it('should sanitize \n chars', done => { + term.onData(e => { + assert.equal(e, '\rfoo\rbar\r'); + done(); + }); + term.paste('\r\nfoo\nbar\r'); + }); + it('should respect bracketed paste mode', done => { + term.onData(e => { + assert.equal(e, '\x1b[200~foo\x1b[201~'); + done(); + }); + term.write('\x1b[?2004h'); + term.paste('foo'); + }); + }); + describe('scroll', () => { describe('scrollLines', () => { let startYDisp: number; @@ -1123,6 +1148,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 ef05c6647..d793abfc8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,7 +25,7 @@ import { IInputHandlingTerminal, ICompositionHelper, ITerminalOptions, ITerminal import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; -import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from 'browser/Clipboard'; +import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { C0 } from 'common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; @@ -39,14 +39,14 @@ import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { DomRenderer } from './renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; @@ -55,12 +55,13 @@ import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; import { LogService } from 'common/services/LogService'; import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; import { DirtyRowService } from 'common/services/DirtyRowService'; import { InstantiationService } from 'common/services/InstantiationService'; +import { CoreMouseService } from 'common/services/CoreMouseService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -112,6 +113,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // common services private _bufferService: IBufferService; private _coreService: ICoreService; + private _coreMouseService: ICoreMouseService; private _dirtyRowService: IDirtyRowService; private _instantiationService: IInstantiationService; private _logService: ILogService; @@ -139,16 +141,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public charsets: ICharset[]; // mouse properties - private _decLocator: boolean; // This is unstable and never set - public x10Mouse: boolean; - public vt200Mouse: boolean; - private _vt300Mouse: boolean; // This is unstable and never set - public normalMouse: boolean; - public mouseEvents: boolean; + public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; public sendFocus: boolean; - public utfMouse: boolean; - public sgrMouse: boolean; - public urxvtMouse: boolean; // misc public savedCols: number; @@ -257,6 +251,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); this._instantiationService.setService(ICoreService, this._coreService); this._coreService.onData(e => this._onData.fire(e)); + this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this._coreMouseService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); @@ -320,7 +316,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._userScrolling = false; // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService); + this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); @@ -394,6 +390,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': @@ -497,7 +494,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } copyHandler(event, this._selectionService); })); - const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this._coreService.triggerDataEvent(e, true)); + const pasteHandlerWrapper = (event: ClipboardEvent) => handlePasteEvent(event, this.textarea, this.bracketedPasteMode, this._coreService); this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); @@ -643,7 +640,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,6 +660,9 @@ 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 if (this.mouseEvents) { this._selectionService.disable(); @@ -671,7 +670,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } else { this._selectionService.enable(); } - this._inputHandler.setBrowserServices(this._selectionService); if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to @@ -691,7 +689,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Listen for mouse events and translate // them into terminal mouse protocols. this.bindMouse(); - } private _createRenderer(): IRenderer { @@ -720,227 +717,188 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } /** - * XTerm mouse events - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * To better understand these - * the xterm code is very helpful: - * Relevant files: - * button.c, charproc.c, misc.c - * Relevant functions in xterm/button.c: - * BtnCode, EmitButtonCode, EditorButton, SendMousePosition + * Bind certain mouse events to the terminal. + * By default only 3 button + wheel up/down is ativated. For higher buttons + * no mouse report will be created. Typically the standard actions will be active. + * + * There are several reasons not to enable support for higher buttons/wheel: + * - Button 4 and 5 are typically used for history back and forward navigation, + * there is no straight forward way to supress/intercept those standard actions. + * - Support for higher buttons does not work in some platform/browser combinations. + * - Left/right wheel was not tested. + * - Emulators vary in mouse button support, typically only 3 buttons and + * wheel up/down work reliable. + * + * TODO: Move mouse event code into its own file. */ public bindMouse(): void { - const el = this.element; const self = this; - let pressed = 32; + const el = this.element; - // mouseup, mousedown, wheel - // left click: ^[[M 3<^[[M#3< - // wheel up: ^[[M`3> - function sendButton(ev: MouseEvent | WheelEvent): void { - let button; + // send event to CoreMouseService + function sendEvent(ev: MouseEvent | WheelEvent): boolean { let pos; - // get the xterm-style button - button = getButton(ev); - // get mouse coordinates pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); - if (!pos) return; - - sendEvent(button, pos); + if (!pos) { + return false; + } + let but: CoreMouseButton; + let action: CoreMouseAction; switch ((ev).overrideType || ev.type) { - case 'mousedown': - pressed = button; + case 'mousemove': + action = CoreMouseAction.MOVE; + if (ev.buttons === undefined) { + // buttons is not supported on macOS, try to get a value from button instead + but = CoreMouseButton.NONE; + if (ev.button !== undefined) { + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + } + } else { + // according to MDN buttons only reports up to button 5 (AUX2) + but = ev.buttons & 1 ? CoreMouseButton.LEFT : + ev.buttons & 4 ? CoreMouseButton.MIDDLE : + ev.buttons & 2 ? CoreMouseButton.RIGHT : + CoreMouseButton.NONE; // fallback to NONE + } break; case 'mouseup': - // keep it at the left - // button, just in case. - pressed = 32; + action = CoreMouseAction.UP; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + break; + case 'mousedown': + action = CoreMouseAction.DOWN; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'wheel': - // nothing. don't - // interfere with - // `pressed`. + // only UP/DOWN wheel events are respected + if ((ev as WheelEvent).deltaY !== 0) { + action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + } + but = CoreMouseButton.WHEEL; break; + default: + // dont handle other event types by accident + return false; } + + // exit if we cannot determine valid button/action values + // do nothing for higher buttons than wheel + if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) { + return false; + } + + return self._coreMouseService.triggerMouseEvent({ + col: pos.x - 33, // FIXME: why -33 here? + row: pos.y - 33, + button: but, + action, + ctrl: ev.ctrlKey, + alt: ev.altKey, + shift: ev.shiftKey + }); } - // motion example of a left click: - // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< - function sendMove(ev: MouseEvent): void { - let button = pressed; - const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); - if (!pos) return; - - // buttons marked as motions - // are incremented by 32 - button += 32; - - sendEvent(button, pos); - } - - // encode button and - // position to characters - function encode(data: number[], ch: number): void { - if (!self.utfMouse) { - if (ch === 255) { - data.push(0); - return; + /** + * Event listener state handling. + * We listen to the onProtocolChange event of CoreMouseService and put + * requested listeners in `requestedEvents`. With this the listeners + * have all bits to do the event listener juggling. + * Note: 'mousedown' currently is "always on" and not managed + * by onProtocolChange. + */ + const requestedEvents: {[key: string]: ((ev: Event) => void) | null} = { + mouseup: null, + wheel: null, + mousedrag: null, + mousemove: null + }; + const eventListeners: {[key: string]: (ev: Event) => void} = { + mouseup: (ev: MouseEvent) => { + sendEvent(ev); + if (!ev.buttons) { + // if no other button is held remove global handlers + this._document.removeEventListener('mouseup', requestedEvents.mouseup); + if (requestedEvents.mousedrag) { + this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + } } - if (ch > 127) ch = 127; - data.push(ch); + return this.cancel(ev); + }, + wheel: (ev: WheelEvent) => { + sendEvent(ev); + ev.preventDefault(); + return this.cancel(ev); + }, + mousedrag: (ev: MouseEvent) => { + // deal only with move while a button is held + if (ev.buttons) { + sendEvent(ev); + } + }, + mousemove: (ev: MouseEvent) => { + // deal only with move without any button + if (!ev.buttons) { + sendEvent(ev); + } + } + }; + this._coreMouseService.onProtocolChange(events => { + // apply global changes on events + this.mouseEvents = events; + if (events) { + if (this.optionsService.options.logLevel === 'debug') { + this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events)); + } + this.element.classList.add('enable-mouse-events'); + this._selectionService.disable(); } else { - if (ch > 2047) { - data.push(2047); - return; - } - data.push(ch); - } - } - - // send a mouse event: - // regular/utf8: ^[[M Cb Cx Cy - // urxvt: ^[[ Cb ; Cx ; Cy M - // sgr: ^[[ Cb ; Cx ; Cy M/m - // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r - // locator: CSI P e ; P b ; P r ; P c ; P p & w - function sendEvent(button: number, pos: {x: number, y: number}): void { - if (self._vt300Mouse) { - // NOTE: Unstable. - // http://www.vt100.net/docs/vt3xx-gp/chapter15.html - button &= 3; - pos.x -= 32; - pos.y -= 32; - let data = C0.ESC + '[24'; - if (button === 0) data += '1'; - else if (button === 1) data += '3'; - else if (button === 2) data += '5'; - else if (button === 3) return; - else data += '0'; - data += '~[' + pos.x + ',' + pos.y + ']\r'; - self._coreService.triggerDataEvent(data, true); - return; + this._logService.debug('Unbinding from mouse events.'); + this.element.classList.remove('enable-mouse-events'); + this._selectionService.enable(); } - if (self._decLocator) { - // NOTE: Unstable. - button &= 3; - pos.x -= 32; - pos.y -= 32; - if (button === 0) button = 2; - else if (button === 1) button = 4; - else if (button === 2) button = 6; - else if (button === 3) button = 3; - self._coreService.triggerDataEvent(C0.ESC + '[' - + button - + ';' - + (button === 3 ? 4 : 0) - + ';' - + pos.y - + ';' - + pos.x - + ';' - // Not sure what page is meant to be - + (pos).page || 0 - + '&w', true); - return; + // add/remove handlers from requestedEvents + + if (!(events & CoreMouseEventType.MOVE)) { + el.removeEventListener('mousemove', requestedEvents.mousemove); + requestedEvents.mousemove = null; + } else if (!requestedEvents.mousemove) { + el.addEventListener('mousemove', eventListeners.mousemove); + requestedEvents.mousemove = eventListeners.mousemove; } - if (self.urxvtMouse) { - pos.x -= 32; - pos.y -= 32; - pos.x++; - pos.y++; - self._coreService.triggerDataEvent(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M', true); - return; + if (!(events & CoreMouseEventType.WHEEL)) { + el.removeEventListener('wheel', requestedEvents.wheel); + requestedEvents.wheel = null; + } else if (!requestedEvents.wheel) { + el.addEventListener('wheel', eventListeners.wheel); + requestedEvents.wheel = eventListeners.wheel; } - if (self.sgrMouse) { - pos.x -= 32; - pos.y -= 32; - self._coreService.triggerDataEvent(C0.ESC + '[<' - + (((button & 3) === 3 ? button & ~3 : button) - 32) - + ';' - + pos.x - + ';' - + pos.y - + ((button & 3) === 3 ? 'm' : 'M'), true); - return; + if (!(events & CoreMouseEventType.UP)) { + this._document.removeEventListener('mouseup', requestedEvents.mouseup); + requestedEvents.mouseup = null; + } else if (!requestedEvents.mouseup) { + requestedEvents.mouseup = eventListeners.mouseup; } - const data: number[] = []; - - encode(data, button); - encode(data, pos.x); - encode(data, pos.y); - - self._coreService.triggerDataEvent(C0.ESC + '[M' + String.fromCharCode.apply(String, data), true); - } - - function getButton(ev: MouseEvent): number { - let button; - let shift; - let meta; - let ctrl; - let mod; - - // two low bits: - // 0 = left - // 1 = middle - // 2 = right - // 3 = release - // wheel up/down: - // 1, and 2 - with 64 added - switch ((ev).overrideType || ev.type) { - case 'mousedown': - button = ev.button !== null && ev.button !== undefined - ? +ev.button - : ev.which !== null && ev.which !== undefined - ? ev.which - 1 - : null; - break; - case 'mouseup': - button = 3; - break; - case 'DOMMouseScroll': - button = ev.detail < 0 - ? 64 - : 65; - break; - case 'wheel': - button = (ev).deltaY < 0 - ? 64 - : 65; - break; + if (!(events & CoreMouseEventType.DRAG)) { + this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + requestedEvents.mousedrag = null; + } else if (!requestedEvents.mousedrag) { + requestedEvents.mousedrag = eventListeners.mousedrag; } + }); + // force initial onProtocolChange so we dont miss early mouse requests + this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol; - // next three bits are the modifiers: - // 4 = shift, 8 = meta, 16 = control - shift = ev.shiftKey ? 4 : 0; - meta = ev.metaKey ? 8 : 0; - ctrl = ev.ctrlKey ? 16 : 0; - mod = shift | meta | ctrl; - - // no mods - if (self.vt200Mouse) { - // ctrl only - mod &= ctrl; - } else if (!self.normalMouse) { - mod = 0; - } - - // increment to SP - button = (32 + (mod << 2)) + button; - - return button; - } - + /** + * "Always on" event listeners. + */ this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { - - // Prevent the focus on the textarea from getting lost - // and make sure we get focused on mousedown ev.preventDefault(); this.focus(); @@ -951,60 +909,24 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return; } - // send the button - sendButton(ev); + sendEvent(ev); - // fix for odd bug - // if (this.vt200Mouse && !this.normalMouse) { - if (this.vt200Mouse) { - (ev).overrideType = 'mouseup'; - sendButton(ev); - return this.cancel(ev); + // Register additional global handlers which should keep reporting outside + // of the terminal element. + // Note: Other emulators also do this for 'mousedown' while a button + // is held, we currently limit 'mousedown' to the terminal only. + if (requestedEvents.mouseup) { + this._document.addEventListener('mouseup', requestedEvents.mouseup); } - - // TODO: All mouse handling should be pulled into its own file. - - // bind events - let moveHandler: (event: MouseEvent) => void; - if (this.normalMouse) { - moveHandler = (event: MouseEvent) => { - // Do nothing if normal mouse mode is on. This can happen if the mouse is held down when the - // terminal exits normalMouse mode. - if (!this.normalMouse) { - return; - } - sendMove(event); - }; - // TODO: these event listeners should be managed by the disposable, the Terminal reference may - // be kept aroud if Terminal.dispose is fired when the mouse is down - this._document.addEventListener('mousemove', moveHandler); + if (requestedEvents.mousedrag) { + this._document.addEventListener('mousemove', requestedEvents.mousedrag); } - // x10 compatibility mode can't send button releases - const handler = (ev: MouseEvent) => { - if (this.normalMouse && !this.x10Mouse) { - sendButton(ev); - } - if (moveHandler) { - // Even though this should only be attached when this.normalMouse is true, holding the - // mouse button down when normalMouse changes can happen. Just always try to remove it. - this._document.removeEventListener('mousemove', moveHandler); - moveHandler = null; - } - this._document.removeEventListener('mouseup', handler); - return this.cancel(ev); - }; - this._document.addEventListener('mouseup', handler); - return this.cancel(ev); })); - // if (this.normalMouse) { - // on(this.document, 'mousemove', sendMove); - // } - this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { - if (!this.mouseEvents) { + if (!requestedEvents.wheel) { // Convert wheel events into up/down events when the buffer does not have scrollback, this // enables scrolling in apps hosted in the alt buffer such as vim or tmux. if (!this.buffer.hasScrollback) { @@ -1025,17 +947,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } return; } - if (this.x10Mouse || this._vt300Mouse || this._decLocator) return; - sendButton(ev); - ev.preventDefault(); })); // allow wheel scrolling in // the shell for example this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { - if (this.mouseEvents) return; - this.viewport.onWheel(ev); - return this.cancel(ev); + if (requestedEvents.wheel) return; + if (!this.viewport.onWheel(ev)) { + return this.cancel(ev); + } })); this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { @@ -1046,11 +966,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { if (this.mouseEvents) return; - this.viewport.onTouchMove(ev); - return this.cancel(ev); + if (!this.viewport.onTouchMove(ev)) { + return this.cancel(ev); + } })); } + /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. @@ -1384,6 +1306,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.write(data + '\r\n'); } + public paste(data: string): void { + paste(data, this.textarea, this.bracketedPasteMode, this._coreService); + } + /** * Attaches a custom key event handler which is run before keys are processed, * giving consumers of xterm.js ultimate control as to what keys should be @@ -1397,9 +1323,19 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._customKeyEventHandler = customKeyEventHandler; } + /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._inputHandler.addEscHandler(id, callback); + } + + /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(id, callback); + } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - return this._inputHandler.addCsiHandler(flag, callback); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(id, callback); } /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { @@ -1816,6 +1752,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._setup(); this._bufferService.reset(); this._coreService.reset(); + this._coreMouseService.reset(); if (this._selectionService) { this._selectionService.reset(); } diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 0ee948d53..ad19f7216 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -6,7 +6,7 @@ import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; @@ -15,7 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { @@ -68,17 +68,26 @@ export class MockTerminal implements ITerminal { writeln(data: string): void { throw new Error('Method not implemented.'); } + paste(data: string): void { + throw new Error('Method not implemented.'); + } open(parent: HTMLElement): void { throw new Error('Method not implemented.'); } attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - throw new Error('Method not implemented.'); + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + throw new Error('Method not implemented.'); } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - throw new Error('Method not implemented.'); + throw new Error('Method not implemented.'); } registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { throw new Error('Method not implemented.'); @@ -210,7 +219,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { x10Mouse: boolean; vt200Mouse: boolean; normalMouse: boolean; - mouseEvents: boolean; + mouseEvents: CoreMouseEventType; sendFocus: boolean; utfMouse: boolean; sgrMouse: boolean; @@ -401,13 +410,13 @@ export class MockViewport implements IViewport { onThemeChange(colors: IColorSet): void { throw new Error('Method not implemented.'); } - onWheel(ev: WheelEvent): void { + onWheel(ev: WheelEvent): boolean { throw new Error('Method not implemented.'); } onTouchStart(ev: TouchEvent): void { throw new Error('Method not implemented.'); } - onTouchMove(ev: TouchEvent): void { + onTouchMove(ev: TouchEvent): boolean { throw new Error('Method not implemented.'); } syncScrollArea(): void { } diff --git a/src/Types.d.ts b/src/Types.d.ts index f96ba07d1..13c65a4d6 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -4,12 +4,12 @@ */ import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm'; -import { ICharset, IAttributeData, CharData } from 'common/Types'; +import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types'; import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -36,14 +36,8 @@ export interface IInputHandlingTerminal { bracketedPasteMode: boolean; curAttrData: IAttributeData; savedCols: number; - x10Mouse: boolean; - vt200Mouse: boolean; - normalMouse: boolean; - mouseEvents: boolean; + mouseEvents: CoreMouseEventType; sendFocus: boolean; - utfMouse: boolean; - sgrMouse: boolean; - urxvtMouse: boolean; cursorHidden: boolean; buffers: IBufferSet; @@ -113,7 +107,8 @@ export interface IInputHandler { /** CSI ` */ charPosAbsolute(params: IParams): void; /** CSI a */ hPositionRelative(params: IParams): void; /** CSI b */ repeatPrecedingCharacter(params: IParams): void; - /** CSI c */ sendDeviceAttributes(params: IParams, collect?: string): void; + /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void; + /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void; /** CSI d */ linePosAbsolute(params: IParams): void; /** CSI e */ vPositionRelative(params: IParams): void; /** CSI f */ hVPosition(params: IParams): void; @@ -200,7 +195,9 @@ export interface IPublicTerminal extends IDisposable { writeln(data: string): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; @@ -223,6 +220,7 @@ export interface IPublicTerminal extends IDisposable { clear(): void; write(data: string): void; writeUtf8(data: Uint8Array): void; + paste(data: string): void; refresh(start: number, end: number): void; reset(): void; } diff --git a/src/browser/Clipboard.ts b/src/browser/Clipboard.ts index f56868ccc..594e4429e 100644 --- a/src/browser/Clipboard.ts +++ b/src/browser/Clipboard.ts @@ -4,6 +4,7 @@ */ import { ISelectionService } from 'browser/services/Services'; +import { ICoreService } from 'common/services/Services'; /** * Prepares text to be pasted into the terminal by normalizing the line endings @@ -41,24 +42,21 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ * @param ev The original paste event to be handled * @param term The terminal on which to apply the handled paste event */ -export function pasteHandler(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, triggerUserInput: (data: string) => void): void { +export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void { ev.stopPropagation(); - - let text: string; - - const dispatchPaste = function(text: string): void { - text = prepareTextForTerminal(text); - text = bracketTextForPaste(text, bracketedPasteMode); - triggerUserInput(text); - textarea.value = ''; - }; - if (ev.clipboardData) { - text = ev.clipboardData.getData('text/plain'); - dispatchPaste(text); + const text = ev.clipboardData.getData('text/plain'); + paste(text, textarea, bracketedPasteMode, coreService); } } +export function paste(text: string, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void { + text = prepareTextForTerminal(text); + text = bracketTextForPaste(text, bracketedPasteMode); + coreService.triggerDataEvent(text, true); + textarea.value = ''; +} + /** * Moves the textarea under the mouse cursor and focuses it. * @param ev The original right click event to be handled. diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 3f3a76376..894beb36c 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -37,9 +37,9 @@ export interface IViewport extends IDisposable { scrollBarWidth: number; syncScrollArea(): void; getLinesScrolled(ev: WheelEvent): number; - onWheel(ev: WheelEvent): void; + onWheel(ev: WheelEvent): boolean; onTouchStart(ev: TouchEvent): void; - onTouchMove(ev: TouchEvent): void; + onTouchMove(ev: TouchEvent): boolean; onThemeChange(colors: IColorSet): void; } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 94bb0fbff..8747297a4 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -153,20 +153,36 @@ export class Viewport extends Disposable implements IViewport { this._scrollLines(diff, true); } + /** + * Handles bubbling of scroll event in case the viewport has reached top or bottom + * @param ev The scroll event. + * @param amount The amount scrolled + */ + private _bubbleScroll(ev: Event, amount: number): boolean { + const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight; + if ((amount < 0 && this._viewportElement.scrollTop !== 0) || + (amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) { + if (ev.cancelable) { + ev.preventDefault(); + } + return false; + } + return true; + } + /** * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual * scrolling to `onScroll`, this event needs to be attached manually by the consumer of * `Viewport`. * @param ev The mouse wheel event. */ - public onWheel(ev: WheelEvent): void { + public onWheel(ev: WheelEvent): boolean { const amount = this._getPixelsScrolled(ev); if (amount === 0) { - return; + return false; } this._viewportElement.scrollTop += amount; - // Prevent the page from scrolling when the terminal scrolls - ev.preventDefault(); + return this._bubbleScroll(ev, amount); } private _getPixelsScrolled(ev: WheelEvent): number { @@ -244,13 +260,13 @@ export class Viewport extends Disposable implements IViewport { * Handles the touchmove event, scrolling the viewport if the position shifted. * @param ev The touch event. */ - public onTouchMove(ev: TouchEvent): void { + public onTouchMove(ev: TouchEvent): boolean { const deltaY = this._lastTouchY - ev.touches[0].pageY; this._lastTouchY = ev.touches[0].pageY; if (deltaY === 0) { - return; + return false; } this._viewportElement.scrollTop += deltaY; - ev.preventDefault(); + return this._bubbleScroll(ev, deltaY); } } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index c0b3c0e57..63084f545 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 90c85ecd7..2efb095d6 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -664,7 +664,7 @@ export class SelectionService implements ISelectionService { this._removeMouseDownListeners(); if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { - if (event.altKey) { + if (event.altKey && this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) { const coordinates = this._mouseService.getCoords( event, this._element, diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index ec36be2fa..f2721bbe9 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; export class MockBufferService implements IBufferService { serviceBrand: any; @@ -29,6 +29,19 @@ export class MockBufferService implements IBufferService { reset(): void {} } +export class MockCoreMouseService implements ICoreMouseService { + activeEncoding: string = ''; + activeProtocol: string = ''; + addEncoding(name: string): void {} + addProtocol(name: string): void {} + reset(): void {} + triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } + onProtocolChange: IEvent = new EventEmitter().event; + explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { + throw new Error('Method not implemented.'); + } +} + export class MockCoreService implements ICoreService { serviceBrand: any; decPrivateModes: IDecPrivateModes = {} as any; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index b25b50a5b..2f7420389 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -158,3 +158,96 @@ export interface IRowRange { start: number; end: number; } + +/** + * Interface for mouse events in the core. + */ +export const enum CoreMouseButton { + LEFT = 0, + MIDDLE = 1, + RIGHT = 2, + NONE = 3, + WHEEL = 4, + // additional buttons 1..8 + // untested! + AUX1 = 8, + AUX2 = 9, + AUX3 = 10, + AUX4 = 11, + AUX5 = 12, + AUX6 = 13, + AUX7 = 14, + AUX8 = 15 +} + +export const enum CoreMouseAction { + UP = 0, // buttons, wheel + DOWN = 1, // buttons, wheel + LEFT = 2, // wheel only + RIGHT = 3, // wheel only + MOVE = 32 // buttons only +} + +export interface ICoreMouseEvent { + /** column (zero based). */ + col: number; + /** row (zero based). */ + row: number; + /** + * Button the action occured. Due to restrictions of the tracking protocols + * it is not possible to report multiple buttons at once. + * Wheel is treated as a button. + * There are invalid combinations of buttons and actions possible + * (like move + wheel), those are silently ignored by the CoreMouseService. + */ + button: CoreMouseButton; + action: CoreMouseAction; + /** + * Modifier states. + * Protocols will add/ignore those based on specific restrictions. + */ + ctrl?: boolean; + alt?: boolean; + shift?: boolean; +} + +/** + * CoreMouseEventType + * To be reported to the browser component which events a mouse + * protocol wants to be catched and forwarded as an ICoreMouseEvent + * to CoreMouseService. + */ +export const enum CoreMouseEventType { + NONE = 0, + /** any mousedown event */ + DOWN = 1, + /** any mouseup event */ + UP = 2, + /** any mousemove event while a button is held */ + DRAG = 4, + /** any mousemove event without a button */ + MOVE = 8, + /** any wheel event */ + WHEEL = 16 +} + +/** + * Mouse protocol interface. + * A mouse protocol can be registered and activated at the CoreMouseService. + * `events` should contain a list of needed events as a hint for the browser component + * to install/remove the appropriate event handlers. + * `restrict` applies further protocol specific restrictions like not allowed + * modifiers or filtering invalid event types. + */ +export interface ICoreMouseProtocol { + events: CoreMouseEventType; + restrict: (e: ICoreMouseEvent) => boolean; +} + +/** + * CoreMouseEncoding + * The tracking encoding can be registered and activated at the CoreMouseService. + * If a ICoreMouseEvent passes all procotol restrictions it will be encoded + * with the active encoding and sent out. + */ +export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index af5abecab..35aade4aa 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 532230ecd..e229d69c3 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/Constants.ts b/src/common/parser/Constants.ts index 55e4a005f..85156c3eb 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -43,3 +43,16 @@ export const enum ParserAction { DCS_PUT = 13, DCS_UNHOOK = 14 } + +/** + * Internal states of OscParser. + */ +export const enum OscState { + START = 0, + ID = 1, + PAYLOAD = 2, + ABORT = 3 +} + +// payload limit for OSC and DCS +export const PAYLOAD_LIMIT = 10000000; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts new file mode 100644 index 000000000..4d6dce110 --- /dev/null +++ b/src/common/parser/DcsParser.test.ts @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +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'; +import { PAYLOAD_LIMIT } from 'common/parser/Constants'; + +function toUtf32(s: string): Uint32Array { + const utf32 = new Uint32Array(s.length); + const decoder = new StringToUtf32(); + const length = decoder.decode(s, utf32); + return utf32.subarray(0, length); +} + +function identifier(id: IFunctionIdentifier): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (0x40 > finalCode || finalCode > 0x7e) { + throw new Error('final must be in range 0x40 .. 0x7e'); + } + res <<= 8; + res |= finalCode; + + return res; +} + +class TestHandler implements IDcsHandler { + constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} + hook(params: IParams): void { + this.output.push([this.msg, 'HOOK', params.toArray()]); + } + put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); + } + unhook(success: boolean): void | boolean { + this.output.push([this.msg, 'UNHOOK', success]); + if (this.returnFalse) { + return false; + } + } +} + +describe('DcsParser', () => { + let parser: DcsParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new DcsParser(); + parser.setHandlerFallback((id, action, data) => { + if (action === 'HOOK') { + data = data.toArray(); + } + reports.push([id, action, data]); + }); + }); + describe('handler registration', () => { + it('setDcsHandler', () => { + 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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['th', 'HOOK', [1, 2, 3]], + ['th', 'PUT', 'Here comes'], + ['th', 'PUT', 'the mouse!'], + ['th', 'UNHOOK', true] + ]); + }); + it('clearDcsHandler', () => { + 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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + // messages from fallback handler + [identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]], + [identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'], + [identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'], + [identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true] + ]); + }); + it('addDcsHandler', () => { + parser.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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + ['th2', 'HOOK', [1, 2, 3]], + ['th1', 'HOOK', [1, 2, 3]], + ['th2', 'PUT', 'Here comes'], + ['th1', 'PUT', 'Here comes'], + ['th2', 'PUT', 'the mouse!'], + ['th1', 'PUT', 'the mouse!'], + ['th2', 'UNHOOK', true], + ['th1', 'UNHOOK', false] // false due being already handled by th2! + ]); + }); + it('addDcsHandler with return false', () => { + 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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + ['th2', 'HOOK', [1, 2, 3]], + ['th1', 'HOOK', [1, 2, 3]], + ['th2', 'PUT', 'Here comes'], + ['th1', 'PUT', 'Here comes'], + ['th2', 'PUT', 'the mouse!'], + ['th1', 'PUT', 'the mouse!'], + ['th2', 'UNHOOK', true], + ['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling + ]); + }); + it('dispose handlers', () => { + 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'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + ['th1', 'HOOK', [1, 2, 3]], + ['th1', 'PUT', 'Here comes'], + ['th1', 'PUT', 'the mouse!'], + ['th1', 'UNHOOK', true] + ]); + }); + }); + describe('DcsHandlerFactory', () => { + it('should be called once on end(true)', () => { + 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); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', () => { + 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); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(false); + assert.deepEqual(reports, []); + }); + it('should be disposable', () => { + 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); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); + dispo.dispose(); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + data = toUtf32('some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); + }); + it('should respect 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); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]); + }); + it('should work up to payload limit', function(): void { + this.timeout(10000); + 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) { + parser.put(data, 0, data.length); + } + parser.unhook(true); + assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(PAYLOAD_LIMIT)]]); + }); + it('should abort for payload limit +1', function(): void { + this.timeout(10000); + 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) { + parser.put(data, 0, data.length); + } + data = toUtf32('A'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, []); + }); + }); +}); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts new file mode 100644 index 000000000..4622c4ade --- /dev/null +++ b/src/common/parser/DcsParser.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/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 _active: IDcsHandler[] = EMPTY_HANDLERS; + private _ident: number = 0; + private _handlerFb: DcsFallbackHandlerType = () => {}; + + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => {}; + } + + public addHandler(ident: number, handler: IDcsHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; + } + const handlerList = this._handlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + + public setHandler(ident: number, handler: IDcsHandler): void { + this._handlers[ident] = [handler]; + } + + public clearHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; + } + + public setHandlerFallback(handler: DcsFallbackHandlerType): void { + this._handlerFb = handler; + } + + public reset(): void { + if (this._active.length) { + this.unhook(false); + } + this._active = EMPTY_HANDLERS; + this._ident = 0; + } + + public hook(ident: number, params: IParams): void { + // always reset leftover handlers + this.reset(); + this._ident = ident; + this._active = this._handlers[ident] || EMPTY_HANDLERS; + if (!this._active.length) { + this._handlerFb(this._ident, 'HOOK', params); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].hook(params); + } + } + } + + public put(data: Uint32Array, start: number, end: number): void { + if (!this._active.length) { + this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); + } + } + } + + public unhook(success: boolean): void { + if (!this._active.length) { + this._handlerFb(this._ident, 'UNHOOK', success); + } else { + let j = this._active.length - 1; + for (; j >= 0; j--) { + if (this._active[j].unhook(success) !== false) { + break; + } + } + j--; + // cleanup left over handlers + for (; j >= 0; j--) { + this._active[j].unhook(false); + } + } + this._active = EMPTY_HANDLERS; + this._ident = 0; + } +} + +/** + * Convenient class to create a DCS handler from a single callback function. + * Note: The payload is currently limited to 50 MB (hardcoded). + */ +export class DcsHandler implements IDcsHandler { + private _data = ''; + private _params: IParams | undefined; + private _hitLimit: boolean = false; + + constructor(private _handler: (data: string, params: IParams) => any) {} + + public hook(params: IParams): void { + this._params = params.clone(); + this._data = ''; + this._hitLimit = false; + } + + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } + this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } + } + + public unhook(success: boolean): any { + let ret; + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._data, this._params ? this._params : new Params()); + } + this._params = undefined; + this._data = ''; + this._hitLimit = false; + return ret; + } +} diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 90ec0ee0f..fa6feb5ce 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,12 +3,15 @@ * @license MIT */ -import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; -import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; import { ParserState } from 'common/parser/Constants'; import { Params } from 'common/parser/Params'; +import { OscHandler } from 'common/parser/OscParser'; +import { IDisposable } from 'common/Types'; +import { DcsHandler } from 'common/parser/DcsParser'; function r(a: number, b: number): string[] { @@ -20,13 +23,46 @@ function r(a: number, b: number): string[] { return arr; } +class MockOscPutParser implements IOscParser { + private _fallback: OscFallbackHandlerType = () => {}; + public data = ''; + public reset(): void { + this.data = ''; + } + public put(data: Uint32Array, start: number, end: number): void { + this.data += utf32ToString(data, start, end); + } + public dispose(): void { } + public start(): void { } + public end(success: boolean): void { + this.data += `, success: ${success}`; + const id = parseInt(this.data.slice(0, this.data.indexOf(';'))); + if (!isNaN(id)) { + this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1)); + } + } + addHandler(ident: number, handler: IOscHandler): IDisposable { + throw new Error('not implemented'); + } + setHandler(ident: number, handler: IOscHandler): void { + throw new Error('not implemented'); + } + clearHandler(ident: number): void { + throw new Error('not implemented'); + } + setHandlerFallback(handler: OscFallbackHandlerType): void { + this._fallback = handler; + } +} +const oscPutParser = new MockOscPutParser(); + // derived parser with access to internal states class TestEscapeSequenceParser extends EscapeSequenceParser { public get osc(): string { - return this._osc; + return (this._oscParser as MockOscPutParser).data; } public set osc(value: string) { - this._osc = value; + (this._oscParser as MockOscPutParser).data = value; } public get params(): ParamsArray { return this._params.toArray(); @@ -38,13 +74,20 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { return this._params; } public get collect(): string { - return this._collect; + return this.identToString(this._collect); } public set collect(value: string) { - this._collect = value; + this._collect = 0; + for (let i = 0; i < value.length; ++i) { + this._collect <<= 8; + this._collect |= value.charCodeAt(i); + } } - public mockActiveDcsHandler(): void { - this._activeDcsHandler = this._dcsHandlerFb; + public mockOscParser(): void { + this._oscParser = oscPutParser; + } + public identifier(id: IFunctionIdentifier): number { + return this._identifier(id); } } @@ -76,34 +119,17 @@ const testTerminal: any = { actionESC: function (collect: string, flag: string): void { this.calls.push(['esc', collect, flag]); }, - actionDCSHook: function (collect: string, params: IParams, flag: string): void { - this.calls.push(['dcs hook', collect, params.toArray(), flag]); + actionDCSHook: function (params: IParams): void { + this.calls.push(['dcs hook', params.toArray()]); }, - actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { - let s = ''; - for (let i = start; i < end; ++i) { - s += stringFromCodePoint(data[i]); - } + actionDCSPrint: function (s: string): void { this.calls.push(['dcs put', s]); }, - actionDCSUnhook: function (): void { - this.calls.push(['dcs unhook']); + actionDCSUnhook: function (success: boolean): void { + this.calls.push(['dcs unhook', success]); } }; -// dcs handler to map dcs actions into the test object `testTerminal` -class DcsTest implements IDcsHandler { - hook(collect: string, params: IParams, flag: number): void { - testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); - } - put(data: Uint32Array, start: number, end: number): void { - testTerminal.actionDCSPrint(data, start, end); - } - unhook(): void { - testTerminal.actionDCSUnhook(); - } -} - const states: number[] = [ ParserState.GROUND, ParserState.ESCAPE, @@ -124,21 +150,35 @@ let state: any; // parser with Uint8Array based transition table const testParser = new TestEscapeSequenceParser(); +testParser.mockOscParser(); testParser.setPrintHandler(testTerminal.print.bind(testTerminal)); -testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +testParser.setCsiHandlerFallback((ident: number, params: IParams) => { + const id = testParser.identToString(ident); + testTerminal.actionCSI(id.slice(0, -1), params, id.slice(-1)); }); -testParser.setEscHandlerFallback((collect: string, flag: number) => { - testTerminal.actionESC(collect, String.fromCharCode(flag)); +testParser.setEscHandlerFallback((ident: number) => { + const id = testParser.identToString(ident); + testTerminal.actionESC(id.slice(0, -1), id.slice(-1)); }); testParser.setExecuteHandlerFallback((code: number) => { testTerminal.actionExecute(String.fromCharCode(code)); }); -testParser.setOscHandlerFallback((identifier: number, data: string) => { +testParser.setOscHandlerFallback((identifier, action, data) => { if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently - else testTerminal.actionOSC('' + identifier + ';' + data); + else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END +}); +testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => { + switch (action) { + case 'HOOK': + testTerminal.actionDCSHook(payload); + break; + case 'PUT': + testTerminal.actionDCSPrint(payload); + break; + case 'UNHOOK': + testTerminal.actionDCSUnhook(payload); + } }); -testParser.setDcsHandlerFallback(new DcsTest()); // translate string based parse calls into typed array based @@ -217,7 +257,8 @@ describe('EscapeSequenceParser', function (): void { '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' ]; const exceptions: { [key: number]: { [key: string]: any[] } } = { - 8: { '\x18': [], '\x1a': [] } // simply abort osc state + 8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING + 13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] } // abort DCS_PASSTHROUGH }; parser.reset(); testTerminal.clear(); @@ -241,12 +282,10 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); for (state in states) { parser.currentState = state; - parser.osc = '#'; parser.params = [23]; parser.collect = '#'; parse(parser, '\x1b'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); @@ -358,24 +397,20 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); // C0 parser.currentState = ParserState.ESCAPE; - parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parse(parser, '['); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parse(parser, '\x9b'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); @@ -909,7 +944,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_ENTRY; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -922,7 +957,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_PARAM; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -935,7 +970,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -949,7 +984,6 @@ describe('EscapeSequenceParser', function (): void { puts = puts.concat(r(0x20, 0x7f)); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.mockActiveDcsHandler(); parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); @@ -990,33 +1024,33 @@ describe('EscapeSequenceParser', function (): void { }); it('OSC', function (): void { test('\x1b]0;abc123€öäü\x07', [ - ['osc', '0;abc123€öäü'] + ['osc', '0;abc123€öäü, success: true'] ], null); }); it('single DCS', function (): void { test('\x1bP1;2;3+$aäbc;däe\x9c', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'äbc;däe'], - ['dcs unhook'] + ['dcs unhook', true] ], null); }); it('multi DCS', function (): void { test('\x1bP1;2;3+$abc;de', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'] ], null); testTerminal.clear(); test('abc\x9c', [ ['dcs put', 'abc'], - ['dcs unhook'] + ['dcs unhook', true] ], true); }); it('print + DCS(C1)', function (): void { test('abc\x901;2;3+$abc;de\x9c', [ ['print', 'abc'], - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'], - ['dcs unhook'] + ['dcs unhook', true] ], null); }); it('print + PM(C1) + print', function (): void { @@ -1026,9 +1060,9 @@ describe('EscapeSequenceParser', function (): void { ], null); }); it('print + OSC(C1) + print', function (): void { - test('abc\x9d123tzf\x9cdefg', [ + test('abc\x9d123;tzf\x9cdefg', [ ['print', 'abc'], - ['osc', '123tzf'], + ['osc', '123;tzf, success: true'], ['print', 'defg'] ], null); }); @@ -1039,9 +1073,9 @@ describe('EscapeSequenceParser', function (): void { ], null); }); it('7bit ST should be swallowed', function (): void { - test('abc\x9d123tzf\x1b\\defg', [ + test('abc\x9d123;tzf\x1b\\defg', [ ['print', 'abc'], - ['osc', '123tzf'], + ['osc', '123;tzf, success: true'], ['print', 'defg'] ], null); }); @@ -1057,9 +1091,35 @@ describe('EscapeSequenceParser', function (): void { it('colon notation in DCS params', function (): void { test('abc\x901;2::55;3+$abc;de\x9c', [ ['print', 'abc'], - ['dcs hook', '+$', [1, 2, [-1, 55], 3], 'a'], + ['dcs hook', [1, 2, [-1, 55], 3]], ['dcs put', 'bc;de'], - ['dcs unhook'] + ['dcs unhook', true] + ], null); + }); + it('CAN should abort DCS', () => { + test('abc\x901;2::55;3+$abc;de\x18', [ + ['print', 'abc'], + ['dcs hook', [1, 2, [-1, 55], 3]], + ['dcs put', 'bc;de'], + ['dcs unhook', false] // false for abort + ], null); + }); + it('SUB should abort DCS', () => { + test('abc\x901;2::55;3+$abc;de\x1a', [ + ['print', 'abc'], + ['dcs hook', [1, 2, [-1, 55], 3]], + ['dcs put', 'bc;de'], + ['dcs unhook', false] // false for abort + ], null); + }); + it('CAN should abort OSC', () => { + test('\x1b]0;abc123€öäü\x18', [ + ['osc', '0;abc123€öäü, success: false'] + ], null); + }); + it('SUB should abort OSC', () => { + test('\x1b]0;abc123€öäü\x1a', [ + ['osc', '0;abc123€öäü, success: false'] ], null); }); }); @@ -1091,7 +1151,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_PASSTHROUGH; parse(parser, '\x901;2;3+$a€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); + testTerminal.compare([['dcs hook', [1, 2, 3]], ['dcs put', '€öäü']]); parser.reset(); testTerminal.clear(); }); @@ -1143,32 +1203,83 @@ describe('EscapeSequenceParser', function (): void { chai.expect(print).equal(''); }); it('ESC handler', function (): void { - parser2.setEscHandler('%G', function (): void { + parser2.setEscHandler({intermediates: '%', final: 'G'}, function (): void { esc.push('%G'); }); - parser2.setEscHandler('E', function (): void { + parser2.setEscHandler({final: 'E'}, function (): void { esc.push('E'); }); parse(parser2, INPUT); chai.expect(esc).eql(['%G', 'E']); - parser2.clearEscHandler('%G'); - parser2.clearEscHandler('%G'); // should not throw + parser2.clearEscHandler({intermediates: '%', final: 'G'}); + parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw clearAccu(); parse(parser2, INPUT); chai.expect(esc).eql(['E']); - parser2.clearEscHandler('E'); + parser2.clearEscHandler({final: 'E'}); clearAccu(); parse(parser2, INPUT); chai.expect(esc).eql([]); }); + describe('ESC custom handlers', () => { + it('prevent fallback', () => { + parser2.setEscHandler({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.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.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.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); + parse(parser2, INPUT); + chai.expect(esc).eql(['custom2 - %G']); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setEscHandler({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'); }); + 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'); }); + const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + dispo.dispose(); + dispo.dispose(); + parse(parser2, INPUT); + chai.expect(esc).eql(['default - %G']); + }); + }); it('CSI handler', function (): void { - parser2.setCsiHandler('m', function (params: IParams, collect: string): void { - csi.push(['m', params.toArray(), collect]); + parser2.setCsiHandler({final: 'm'}, function (params: IParams): void { + csi.push(['m', params.toArray(), '']); }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - parser2.clearCsiHandler('m'); - parser2.clearCsiHandler('m'); // should not throw + parser2.clearCsiHandler({final: 'm'}); + parser2.clearCsiHandler({final: 'm'}); // should not throw clearAccu(); parse(parser2, INPUT); chai.expect(csi).eql([]); @@ -1176,16 +1287,16 @@ describe('EscapeSequenceParser', function (): void { describe('CSI custom handlers', () => { it('Prevent fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); it('Allow fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return false; }); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1193,9 +1304,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return false; }); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1204,9 +1315,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([], 'Should not fallback once'); @@ -1214,16 +1325,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setCsiHandler('m', () => order.push(1)); - parser2.addCsiHandler('m', () => { order.push(2); return false; }); - parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parser2.setCsiHandler({final: 'm'}, () => { order.push(1); }); + parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; }); + parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); + const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1231,8 +1342,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); + const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1256,9 +1367,9 @@ describe('EscapeSequenceParser', function (): void { chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { - parser2.setOscHandler(1, function (data: string): void { + parser2.setOscHandler(1, new OscHandler(function (data: string): void { osc.push([1, data]); - }); + })); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); parser2.clearOscHandler(1); @@ -1270,16 +1381,16 @@ describe('EscapeSequenceParser', function (): void { describe('OSC custom handlers', () => { it('Prevent fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, 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, data => osc.push([1, data])); - parser2.addOscHandler(1, 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']]); @@ -1287,9 +1398,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parser2.addOscHandler(1, 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']]); @@ -1298,9 +1409,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parser2.addOscHandler(1, 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'); @@ -1308,16 +1419,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setOscHandler(1, () => order.push(1)); - parser2.addOscHandler(1, () => { order.push(2); return false; }); - parser2.addOscHandler(1, () => { 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, data => osc.push([1, data])); - const customHandler = parser2.addOscHandler(1, 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']]); @@ -1325,8 +1436,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - const customHandler = parser2.addOscHandler(1, 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); @@ -1335,9 +1446,9 @@ describe('EscapeSequenceParser', function (): void { }); }); it('DCS handler', function (): void { - parser2.setDcsHandler('+p', { - hook: function (collect: string, params: IParams, flag: number): void { - dcs.push(['hook', collect, params.toArray(), flag]); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, { + hook: function (params: IParams): void { + dcs.push(['hook', '', params.toArray(), 0]); }, put: function (data: Uint32Array, start: number, end: number): void { let s = ''; @@ -1353,17 +1464,75 @@ describe('EscapeSequenceParser', function (): void { parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); chai.expect(dcs).eql([ - ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], + ['hook', '', [1, 2, 3], 0], ['put', 'abc'], ['put', ';de'], ['unhook'] ]); - parser2.clearDcsHandler('+p'); - parser2.clearDcsHandler('+p'); // should not throw + parser2.clearDcsHandler({intermediates: '+', final: 'p'}); + parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw clearAccu(); parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); chai.expect(dcs).eql([]); }); + describe('DCS custom handlers', () => { + const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; + it('Prevent fallback', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + 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 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 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 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 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 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 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); + chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); + }); + }); it('ERROR handler', function (): void { let errorState: IParsingState | null = null; parser2.setErrorHandler(function (state: IParsingState): IParsingState { @@ -1375,8 +1544,7 @@ describe('EscapeSequenceParser', function (): void { position: 6, code: '€'.charCodeAt(0), currentState: ParserState.CSI_PARAM, - osc: '', - collect: '', + collect: 0, params: Params.fromArray([1, 2, 0]), // extra zero here abort: false }); @@ -1387,5 +1555,110 @@ describe('EscapeSequenceParser', function (): void { chai.expect(errorState).eql(null); }); }); + describe('function identifiers', () => { + describe('registration limits', () => { + it('prefix range 0x3c .. 0x3f, one byte', () => { + for (let i = 0x3c; i <= 0x3f; ++i) { + const c = String.fromCharCode(i); + chai.expect(parser.identToString(parser.identifier({prefix: c, final: 'z'}))).eql(c + 'z'); + } + chai.assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); + chai.assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); + chai.assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported'); + }); + it('intermediates range 0x20 .. 0x2f, up to two bytes', () => { + for (let i = 0x20; i <= 0x2f; ++i) { + const c = String.fromCharCode(i); + chai.expect(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'}))).eql(c + c + 'z'); + } + chai.assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); + chai.assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); + chai.assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported'); + }); + it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => { + for (let i = 0x40; i <= 0x7e; ++i) { + const c = String.fromCharCode(i); + chai.expect(parser.identToString(parser.identifier({final: c}))).eql(c); + } + chai.assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126'); + chai.assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126'); + chai.assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte'); + }); + it('final ESC range 0x30 .. 0x7e, one byte', () => { + for (let i = 0x30; i <= 0x7e; ++i) { + const c = String.fromCharCode(i); + let handler: IDisposable | undefined; + chai.assert.doesNotThrow(() => { handler = parser.addEscHandler({final: c}, () => {}); }, 'final must be in range 48 .. 126'); + if (handler) handler.dispose(); + } + chai.assert.throws(() => { parser.addEscHandler({final: '\x2f'}, () => {}); }, 'final must be in range 48 .. 126'); + chai.assert.throws(() => { parser.addEscHandler({final: '\x7f'}, () => {}); }, 'final must be in range 48 .. 126'); + }); + it('id calculation - should stacking prefix -> intermediate -> final', () => { + chai.expect(parser.identToString(parser.identifier({final: 'z'}))).eql('z'); + chai.expect(parser.identToString(parser.identifier({prefix: '?', final: 'z'}))).eql('?z'); + chai.expect(parser.identToString(parser.identifier({intermediates: '!', final: 'z'}))).eql('!z'); + chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'}))).eql('?!z'); + chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'}))).eql('?!!z'); + }); + }); + describe('identifier invocation', () => { + it('ESC', () => { + const callstack: string[] = []; + const h1 = parser.addEscHandler({final: 'z'}, () => { callstack.push('z'); }); + const h2 = parser.addEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); }); + const h3 = parser.addEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); }); + parse(parser, '\x1bz\x1b!z\x1b!!z'); + h1.dispose(); + h2.dispose(); + h3.dispose(); + parse(parser, '\x1bz\x1b!z\x1b!!z'); + chai.expect(callstack).eql(['z', '!z', '!!z']); + }); + it('CSI', () => { + const callstack: any[] = []; + const h1 = parser.addCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); }); + const h2 = parser.addCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); }); + const h3 = parser.addCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); }); + const h4 = parser.addCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); }); + const h5 = parser.addCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); }); + const h6 = parser.addCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); }); + parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); + h1.dispose(); + h2.dispose(); + h3.dispose(); + h4.dispose(); + h5.dispose(); + h6.dispose(); + parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); + chai.expect(callstack).eql([['z', [1, 0]], ['!z', [1, 0]], ['!!z', [1, 0]], ['?z', [1, 0]], ['?!z', [1, 0]], ['?!!z', [1, 0]]]); + }); + it('DCS', () => { + const callstack: any[] = []; + const h1 = parser.addDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); })); + const h2 = parser.addDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); })); + const h3 = parser.addDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); })); + const h4 = parser.addDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); })); + const h5 = parser.addDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); })); + const h6 = parser.addDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); })); + parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); + h1.dispose(); + h2.dispose(); + h3.dispose(); + h4.dispose(); + h5.dispose(); + h6.dispose(); + parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); + chai.expect(callstack).eql([ + ['z', [1, 0], 'AB'], + ['!z', [1, 0], 'AB'], + ['!!z', [1, 0], 'AB'], + ['?z', [1, 0], 'AB'], + ['?!z', [1, 0], 'AB'], + ['?!!z', [1, 0], 'AB'] + ]); + }); + }); + }); // TODO: error conditions and error recovery (not implemented yet in parser) }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 1d2b7e1e4..bc0b620eb 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,20 +3,14 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } 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 { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; import { Params } from 'common/parser/Params'; - -interface IHandlerCollection { - [key: string]: T[]; -} - -type CsiHandler = (params: IParams, collect: string) => boolean | void; -type OscHandler = (data: string) => boolean | void; +import { OscParser } from 'common/parser/OscParser'; +import { DcsParser } from 'common/parser/DcsParser'; /** * Table values are generated like this: @@ -193,7 +187,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); - table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); // special handling of unicode chars table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); @@ -203,14 +197,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { return table; })(); -/** - * Dummy DCS handler as default fallback. - */ -class DcsDummy implements IDcsHandler { - hook(collect: string, params: IParams, flag: number): void { } - put(data: Uint32Array, start: number, end: number): void { } - unhook(): void { } -} /** * EscapeSequenceParser. @@ -220,17 +206,25 @@ class DcsDummy implements IDcsHandler { * To implement custom ANSI compliant escape sequences it is not needed to * alter this parser, instead consider registering a custom handler. * For non ANSI compliant sequences change the transition table with - * the optional `transitions` contructor argument and + * the optional `transitions` constructor argument and * reimplement the `parse` method. * * This parser is currently hardcoded to operate in ZDM (Zero Default Mode) * as suggested by the original parser, thus empty parameters are set to 0. - * This this is not in line with the latest ECMA specification + * This this is not in line with the latest ECMA-48 specification * (ZDM was part of the early specs and got completely removed later on). * * Other than the original parser from vt100.net this parser supports * sub parameters in digital parameters separated by colons. Empty sub parameters - * are set to -1. + * are set to -1 (no ZDM for sub parameters). + * + * About prefix and intermediate bytes: + * This parser follows the assumptions of the vt100.net parser with these restrictions: + * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f + * - max. two intermediates are respected, byte range 0x20 .. 0x2f + * Note that this is not in line with ECMA-48 which does not limit either of those. + * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently + * there are no known sequences that follow the broader definition of the specification. * * TODO: implement error recovery hook via error handler return values */ @@ -240,27 +234,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public precedingCodepoint: number; // buffers over several parse calls - protected _osc: string; protected _params: Params; - protected _collect: string; + protected _collect: number; // handler lookup containers - protected _printHandler: (data: Uint32Array, start: number, end: number) => void; - protected _executeHandlers: any; - protected _csiHandlers: IHandlerCollection; - protected _escHandlers: any; - protected _oscHandlers: IHandlerCollection; - protected _dcsHandlers: any; - protected _activeDcsHandler: IDcsHandler | null; + 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: (collect: string, params: IParams, flag: number) => void; - protected _escHandlerFb: (collect: string, flag: number) => void; - protected _oscHandlerFb: (identifier: number, data: string) => void; - protected _dcsHandlerFb: IDcsHandler; + protected _printHandlerFb: PrintFallbackHandlerType; + protected _executeHandlerFb: ExecuteFallbackHandlerType; + protected _csiHandlerFb: CsiFallbackHandlerType; + protected _escHandlerFb: EscFallbackHandlerType; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -268,145 +258,197 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.initialState = ParserState.GROUND; this.currentState = this.initialState; - this._osc = ''; this._params = new Params(); // defaults to 32 storable params/subparams this._params.addParam(0); // ZDM - this._collect = ''; + this._collect = 0; this.precedingCodepoint = 0; // set default fallback handlers and handler lookup containers this._printHandlerFb = (data, start, end): void => { }; this._executeHandlerFb = (code: number): void => { }; - this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { }; - this._escHandlerFb = (collect: string, flag: number): void => { }; - this._oscHandlerFb = (identifier: number, data: string): void => { }; - this._dcsHandlerFb = new DcsDummy(); + this._csiHandlerFb = (ident: number, params: IParams): void => { }; + this._escHandlerFb = (ident: number): void => { }; this._errorHandlerFb = (state: IParsingState): IParsingState => state; this._printHandler = this._printHandlerFb; this._executeHandlers = Object.create(null); this._csiHandlers = Object.create(null); this._escHandlers = Object.create(null); - this._oscHandlers = Object.create(null); - this._dcsHandlers = Object.create(null); - this._activeDcsHandler = null; + this._oscParser = new OscParser(); + this._dcsParser = new DcsParser(); this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) - this.setEscHandler('\\', () => {}); + this.setEscHandler({final: '\\'}, () => {}); + } + + protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (finalRange[0] > finalCode || finalCode > finalRange[1]) { + throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`); + } + res <<= 8; + res |= finalCode; + + return res; + } + + public identToString(ident: number): string { + const res: string[] = []; + while (ident) { + res.push(String.fromCharCode(ident & 0xFF)); + ident >>= 8; + } + return res.reverse().join(''); } public dispose(): void { - this._executeHandlers = null; - this._escHandlers = null; - this._dcsHandlers = null; - this._activeDcsHandler = null; + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); + this._escHandlers = Object.create(null); + this._oscParser.dispose(); + this._dcsParser.dispose(); } - setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { - this._printHandler = callback; + public setPrintHandler(handler: PrintHandlerType): void { + this._printHandler = handler; } - clearPrintHandler(): void { + public clearPrintHandler(): void { this._printHandler = this._printHandlerFb; } - setExecuteHandler(flag: string, callback: () => void): void { - this._executeHandlers[flag.charCodeAt(0)] = callback; - } - clearExecuteHandler(flag: string): void { - if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; - } - setExecuteHandlerFallback(callback: (code: number) => void): void { - this._executeHandlerFb = callback; - } - - addCsiHandler(flag: string, callback: CsiHandler): IDisposable { - const index = flag.charCodeAt(0); - if (this._csiHandlers[index] === undefined) { - this._csiHandlers[index] = []; + 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[index]; - 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); } } }; } - setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = [callback]; + public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void { + this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler]; } - clearCsiHandler(flag: string): void { - if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; + public clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } - setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void { + 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)]; + } + public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { this._csiHandlerFb = callback; } - setEscHandler(collectAndFlag: string, callback: () => void): void { - this._escHandlers[collectAndFlag] = callback; + public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.addHandler(this._identifier(id), handler); } - clearEscHandler(collectAndFlag: string): void { - if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag]; + public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { + this._dcsParser.setHandler(this._identifier(id), handler); } - setEscHandlerFallback(callback: (collect: string, flag: number) => void): void { - this._escHandlerFb = callback; + public clearDcsHandler(id: IFunctionIdentifier): void { + this._dcsParser.clearHandler(this._identifier(id)); + } + public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void { + this._dcsParser.setHandlerFallback(handler); } - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - if (this._oscHandlers[ident] === undefined) { - this._oscHandlers[ident] = []; - } - const handlerList = this._oscHandlers[ident]; - handlerList.push(callback); - return { - dispose: () => { - const handlerIndex = handlerList.indexOf(callback); - if (handlerIndex !== -1) { - handlerList.splice(handlerIndex, 1); - } - } - }; + public addOscHandler(ident: number, handler: IOscHandler): IDisposable { + return this._oscParser.addHandler(ident, handler); } - setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = [callback]; + public setOscHandler(ident: number, handler: IOscHandler): void { + this._oscParser.setHandler(ident, handler); } - clearOscHandler(ident: number): void { - if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; + public clearOscHandler(ident: number): void { + this._oscParser.clearHandler(ident); } - setOscHandlerFallback(callback: (identifier: number, data: string) => void): void { - this._oscHandlerFb = callback; + public setOscHandlerFallback(handler: OscFallbackHandlerType): void { + this._oscParser.setHandlerFallback(handler); } - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._dcsHandlers[collectAndFlag] = handler; - } - clearDcsHandler(collectAndFlag: string): void { - if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag]; - } - setDcsHandlerFallback(handler: IDcsHandler): void { - this._dcsHandlerFb = handler; - } - - setErrorHandler(callback: (state: IParsingState) => IParsingState): void { + public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { this._errorHandler = callback; } - clearErrorHandler(): void { + public clearErrorHandler(): void { this._errorHandler = this._errorHandlerFb; } - reset(): void { + public reset(): void { this.currentState = this.initialState; - this._osc = ''; + this._oscParser.reset(); + this._dcsParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM - this._collect = ''; - this._activeDcsHandler = null; + this._collect = 0; this.precedingCodepoint = 0; } + + /** * Parse UTF32 codepoints in `data` up to `length`. * @@ -421,16 +463,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * - OSC_STRING:OSC_PUT * - DCS_PASSTHROUGH:DCS_PUT */ - parse(data: Uint32Array, length: number): void { + public parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; let currentState = this.currentState; - let osc = this._osc; + const osc = this._oscParser; + const dcs = this._dcsParser; let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - let dcsHandler: IDcsHandler | null = this._activeDcsHandler; - let callback: Function | null = null; // process input string for (let i = 0; i < length; ++i) { @@ -466,8 +507,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.EXECUTE: - callback = this._executeHandlers[code]; - if (callback) callback(); + if (this._executeHandlers[code]) this._executeHandlers[code](); else this._executeHandlerFb(code); this.precedingCodepoint = 0; break; @@ -479,7 +519,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP position: i, code, currentState, - osc, collect, params, abort: false @@ -489,142 +528,107 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.CSI_DISPATCH: // Trigger CSI Handler - const handlers = this._csiHandlers[code]; + const handlers = this._csiHandlers[collect << 8 | code]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { // undefined or true means success and to stop bubbling - if (handlers[j](params, collect) !== false) { + if (handlers[j](params) !== false) { break; } } if (j < 0) { - this._csiHandlerFb(collect, params, code); + this._csiHandlerFb(collect << 8 | code, params); } this.precedingCodepoint = 0; break; case ParserAction.PARAM: // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a) - let isSub = false; do { switch (code) { case 0x3b: params.addParam(0); // ZDM - isSub = false; break; case 0x3a: params.addSubParam(-1); - isSub = true; break; default: // 0x30 - 0x39 - if (isSub) params.addSubParamDigit(code - 48); - else params.addParamDigit(code - 48); + params.addDigit(code - 48); } } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c); i--; break; case ParserAction.COLLECT: - collect += String.fromCharCode(code); + collect <<= 8; + collect |= code; break; case ParserAction.ESC_DISPATCH: - callback = this._escHandlers[collect + String.fromCharCode(code)]; - if (callback) callback(collect, code); - else this._escHandlerFb(collect, code); + const handlersEsc = this._escHandlers[collect << 8 | code]; + let jj = handlersEsc ? handlersEsc.length - 1 : -1; + for (; jj >= 0; jj--) { + // undefined or true means success and to stop bubbling + if (handlersEsc[jj]() !== false) { + break; + } + } + if (jj < 0) { + this._escHandlerFb(collect << 8 | code); + } this.precedingCodepoint = 0; break; case ParserAction.CLEAR: - osc = ''; params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; break; case ParserAction.DCS_HOOK: - dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)]; - if (!dcsHandler) dcsHandler = this._dcsHandlerFb; - dcsHandler.hook(collect, params, code); + dcs.hook(collect << 8 | code, params); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f - // unhook triggered by: 0x1b, 0x9c + // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort) for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - if (dcsHandler) { - dcsHandler.put(data, i, j); - } + dcs.put(data, i, j); i = j - 1; break; } } break; case ParserAction.DCS_UNHOOK: - if (dcsHandler) { - dcsHandler.unhook(); - dcsHandler = null; - } + dcs.unhook(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc = ''; params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; this.precedingCodepoint = 0; break; case ParserAction.OSC_START: - osc = ''; + osc.start(); break; case ParserAction.OSC_PUT: // inner loop: 0x20 (SP) included, 0x7F (DEL) included for (let j = i + 1; ; j++) { if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc += utf32ToString(data, i, j); + osc.put(data, i, j); i = j - 1; break; } } break; case ParserAction.OSC_END: - if (osc && code !== 0x18 && code !== 0x1a) { - // NOTE: OSC subparsing is not part of the original parser - // we do basic identifier parsing here to offer a jump table for OSC as well - const idx = osc.indexOf(';'); - if (idx === -1) { - this._oscHandlerFb(-1, osc); // this is an error (malformed OSC) - } else { - // Note: NaN is not handled here - // either catch it with the fallback handler - // or with an explicit NaN OSC handler - const identifier = parseInt(osc.substring(0, idx)); - const content = osc.substring(idx + 1); - // Trigger OSC Handler - const handlers = this._oscHandlers[identifier]; - let j = handlers ? handlers.length - 1 : -1; - for (; j >= 0; j--) { - // undefined or true means success and to stop bubbling - if (handlers[j](content) !== false) { - break; - } - } - if (j < 0) { - this._oscHandlerFb(identifier, content); - } - } - } + osc.end(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc = ''; params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; this.precedingCodepoint = 0; break; } currentState = transition & TableAccess.TRANSITION_STATE_MASK; } - // save non pushable buffers - this._osc = osc; + // save collected intermediates this._collect = collect; - this._params = params; - - // save active dcs handler reference - this._activeDcsHandler = dcsHandler; // save state this.currentState = currentState; diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts new file mode 100644 index 000000000..6969d6f33 --- /dev/null +++ b/src/common/parser/OscParser.test.ts @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +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'; + +function toUtf32(s: string): Uint32Array { + const utf32 = new Uint32Array(s.length); + const decoder = new StringToUtf32(); + const length = decoder.decode(s, utf32); + return utf32.subarray(0, length); +} + +class TestHandler implements IOscHandler { + constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {} + start(): void { + this.output.push([this.msg, this.id, 'START']); + } + put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); + } + end(success: boolean): void | boolean { + this.output.push([this.msg, this.id, 'END', success]); + if (this.returnFalse) { + return false; + } + } +} + +describe('OscParser', () => { + let parser: OscParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new OscParser(); + parser.setHandlerFallback((id, action, data) => { + reports.push([id, action, data]); + }); + }); + describe('identifier parsing', () => { + it('no report for illegal ids', () => { + const data = toUtf32('hello world!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, []); + }); + it('no payload', () => { + parser.start(); + let data = toUtf32('12'); + parser.put(data, 0, data.length); + data = toUtf32('34'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]); + }); + it('with payload', () => { + parser.start(); + let data = toUtf32('12'); + parser.put(data, 0, data.length); + data = toUtf32('34'); + parser.put(data, 0, data.length); + data = toUtf32(';h'); + parser.put(data, 0, data.length); + data = toUtf32('ello'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + [1234, 'START', undefined], + [1234, 'PUT', 'h'], + [1234, 'PUT', 'ello'], + [1234, 'END', true] + ]); + }); + }); + describe('handler registration', () => { + it('setOscHandler', () => { + parser.setHandler(1234, new TestHandler(1234, reports, 'th')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['th', 1234, 'START'], + ['th', 1234, 'PUT', 'Here comes'], + ['th', 1234, 'PUT', 'the mouse!'], + ['th', 1234, 'END', true] + ]); + }); + it('clearOscHandler', () => { + 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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + // messages from fallback handler + [1234, 'START', undefined], + [1234, 'PUT', 'Here comes'], + [1234, 'PUT', 'the mouse!'], + [1234, 'END', true] + ]); + }); + it('addOscHandler', () => { + 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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + ['th2', 1234, 'START'], + ['th1', 1234, 'START'], + ['th2', 1234, 'PUT', 'Here comes'], + ['th1', 1234, 'PUT', 'Here comes'], + ['th2', 1234, 'PUT', 'the mouse!'], + ['th1', 1234, 'PUT', 'the mouse!'], + ['th2', 1234, 'END', true], + ['th1', 1234, 'END', false] // false due being already handled by th2! + ]); + }); + it('addOscHandler with return false', () => { + 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); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + ['th2', 1234, 'START'], + ['th1', 1234, 'START'], + ['th2', 1234, 'PUT', 'Here comes'], + ['th1', 1234, 'PUT', 'Here comes'], + ['th2', 1234, 'PUT', 'the mouse!'], + ['th1', 1234, 'PUT', 'the mouse!'], + ['th2', 1234, 'END', true], + ['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling + ]); + }); + it('dispose handlers', () => { + 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'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + ['th1', 1234, 'START'], + ['th1', 1234, 'PUT', 'Here comes'], + ['th1', 1234, 'PUT', 'the mouse!'], + ['th1', 1234, 'END', true] + ]); + }); + }); + describe('OscHandlerFactory', () => { + it('should be called once on end(true)', () => { + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', () => { + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(false); + assert.deepEqual(reports, []); + }); + it('should be disposable', () => { + 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); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!']]); + dispo.dispose(); + parser.start(); + data = toUtf32('1234;some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); + }); + it('should respect 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); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]); + }); + it('should work up to payload limit', function(): void { + this.timeout(10000); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;'); + parser.put(data, 0, data.length); + data = toUtf32('A'.repeat(1000)); + for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { + parser.put(data, 0, data.length); + } + parser.end(true); + assert.deepEqual(reports, [[1234, 'A'.repeat(PAYLOAD_LIMIT)]]); + }); + it('should abort for payload limit +1', function(): void { + this.timeout(10000); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;'); + parser.put(data, 0, data.length); + data = toUtf32('A'.repeat(1000)); + for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { + parser.put(data, 0, data.length); + } + data = toUtf32('A'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, []); + }); + }); +}); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts new file mode 100644 index 000000000..e8c5a801f --- /dev/null +++ b/src/common/parser/OscParser.ts @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types'; +import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; +import { utf32ToString } from 'common/input/TextDecoder'; +import { IDisposable } from 'common/Types'; + + +export class OscParser implements IOscParser { + private _state = OscState.START; + private _id = -1; + private _handlers: IHandlerCollection = Object.create(null); + private _handlerFb: OscFallbackHandlerType = () => { }; + + public addHandler(ident: number, handler: IOscHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; + } + const handlerList = this._handlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public setHandler(ident: number, handler: IOscHandler): void { + this._handlers[ident] = [handler]; + } + public clearHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; + } + public setHandlerFallback(handler: OscFallbackHandlerType): void { + this._handlerFb = handler; + } + + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => {}; + } + + public reset(): void { + // cleanup handlers if payload was already sent + if (this._state === OscState.PAYLOAD) { + this.end(false); + } + this._id = -1; + this._state = OscState.START; + } + + private _start(): void { + const handlers = this._handlers[this._id]; + if (!handlers) { + this._handlerFb(this._id, 'START'); + } else { + for (let j = handlers.length - 1; j >= 0; j--) { + handlers[j].start(); + } + } + } + + private _put(data: Uint32Array, start: number, end: number): void { + const handlers = this._handlers[this._id]; + if (!handlers) { + this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = handlers.length - 1; j >= 0; j--) { + handlers[j].put(data, start, end); + } + } + } + + private _end(success: boolean): void { + // other than the old code we always have to call .end + // to keep the bubbling we use `success` to indicate + // whether a handler should execute + const handlers = this._handlers[this._id]; + if (!handlers) { + this._handlerFb(this._id, 'END', success); + } else { + let j = handlers.length - 1; + for (; j >= 0; j--) { + if (handlers[j].end(success) !== false) { + break; + } + } + j--; + // cleanup left over handlers + for (; j >= 0; j--) { + handlers[j].end(false); + } + } + } + + public start(): void { + // always reset leftover handlers + this.reset(); + this._id = -1; + this._state = OscState.ID; + } + + /** + * Put data to current OSC command. + * Expects the identifier of the OSC command in the form + * OSC id ; payload ST/BEL + * Payload chunks are not further processed and get + * directly passed to the handlers. + */ + public put(data: Uint32Array, start: number, end: number): void { + if (this._state === OscState.ABORT) { + return; + } + if (this._state === OscState.ID) { + while (start < end) { + const code = data[start++]; + if (code === 0x3b) { + this._state = OscState.PAYLOAD; + this._start(); + break; + } + if (code < 0x30 || 0x39 < code) { + this._state = OscState.ABORT; + return; + } + if (this._id === -1) { + this._id = 0; + } + this._id = this._id * 10 + code - 48; + } + } + if (this._state === OscState.PAYLOAD && end - start > 0) { + this._put(data, start, end); + } + } + + /** + * Indicates end of an OSC command. + * Whether the OSC got aborted or finished normally + * is indicated by `success`. + */ + public end(success: boolean): void { + if (this._state === OscState.START) { + return; + } + // do nothing if command was faulty + if (this._state !== OscState.ABORT) { + // if we are still in ID state and get an early end + // means that the command has no payload thus we still have + // to announce START and send END right after + if (this._state === OscState.ID) { + this._start(); + } + this._end(success); + } + this._id = -1; + this._state = OscState.START; + } +} + +/** + * Convenient class to allow attaching string based handler functions + * as OSC handlers. + */ +export class OscHandler implements IOscHandler { + private _data = ''; + private _hitLimit: boolean = false; + + constructor(private _handler: (data: string) => any) {} + + public start(): void { + this._data = ''; + this._hitLimit = false; + } + + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } + this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } + } + + public end(success: boolean): any { + let ret; + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._data); + } + this._data = ''; + this._hitLimit = false; + return ret; + } +} diff --git a/src/common/parser/Params.test.ts b/src/common/parser/Params.test.ts index 01287b8f7..2418fa452 100644 --- a/src/common/parser/Params.test.ts +++ b/src/common/parser/Params.test.ts @@ -16,27 +16,29 @@ class TestParams extends Params { } /** `Params` parser shim */ -function parse(params: Params, s: string): void { +function parse(params: Params, s: string | string[]): void { params.reset(); params.addParam(0); - let isSub = false; - for (let i = 0; i < s.length; ++i) { - let code = s.charCodeAt(i); - do { - switch (code) { - case 0x3b: - params.addParam(0); - isSub = false; - break; - case 0x3a: - params.addSubParam(-1); - isSub = true; - break; - default: // 0x30 - 0x39 - if (isSub) params.addSubParamDigit(code - 48); - else params.addParamDigit(code - 48); - } - } while (++i < s.length && (code = s.charCodeAt(i)) > 0x2f && code < 0x3c); + if (typeof s === 'string') { + s = [s]; + } + for (const chunk of s) { + for (let i = 0; i < chunk.length; ++i) { + let code = chunk.charCodeAt(i); + do { + switch (code) { + case 0x3b: + params.addParam(0); + break; + case 0x3a: + params.addSubParam(-1); + break; + default: // 0x30 - 0x39 + params.addDigit(code - 48); + } + } while (++i < s.length && (code = chunk.charCodeAt(i)) > 0x2f && code < 0x3c); + i--; + } } } @@ -217,4 +219,19 @@ describe('Params', () => { assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]); }); }); + describe('issue 2389', () => { + it('should cancel subdigits if beyond params limit', () => { + const params = new Params(); + parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::'); + assert.deepEqual(params.toArray(), [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32]); + }); + it('should carry forward isSub state', () => { + const params = new Params(); + parse(params, ['1:22:33', '44']); + assert.deepEqual(params.toArray(), [1, [22, 3344]]); + }); + }); }); diff --git a/src/common/parser/Params.ts b/src/common/parser/Params.ts index 7ef8341a7..47ee129d4 100644 --- a/src/common/parser/Params.ts +++ b/src/common/parser/Params.ts @@ -41,6 +41,7 @@ export class Params implements IParams { private _subParamsIdx: Uint16Array; private _rejectDigits: boolean; private _rejectSubDigits: boolean; + private _digitIsSub: boolean; /** * Create a `Params` type from JS array representation. @@ -79,6 +80,7 @@ export class Params implements IParams { this._subParamsIdx = new Uint16Array(maxLength); this._rejectDigits = false; this._rejectSubDigits = false; + this._digitIsSub = false; } /** @@ -91,6 +93,9 @@ export class Params implements IParams { newParams._subParams.set(this._subParams); newParams._subParamsLength = this._subParamsLength; newParams._subParamsIdx.set(this._subParamsIdx); + newParams._rejectDigits = this._rejectDigits; + newParams._rejectSubDigits = this._rejectSubDigits; + newParams._digitIsSub = this._digitIsSub; return newParams; } @@ -121,6 +126,7 @@ export class Params implements IParams { this._subParamsLength = 0; this._rejectDigits = false; this._rejectSubDigits = false; + this._digitIsSub = false; } /** @@ -131,6 +137,7 @@ export class Params implements IParams { * store up to 30. */ public addParam(value: number): void { + this._digitIsSub = false; if (this.length >= this.maxLength) { this._rejectDigits = true; return; @@ -150,10 +157,11 @@ export class Params implements IParams { * sub parameter will be ignored. */ public addSubParam(value: number): void { + this._digitIsSub = true; if (!this.length) { return; } - if (this._subParamsLength >= this.maxSubParamsLength) { + if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) { this._rejectSubDigits = true; return; } @@ -204,30 +212,18 @@ export class Params implements IParams { /** * Add a single digit value to current parameter. * This is used by the parser to account digits on a char by char basis. - * Do not use this method directly, consider using `addParam` instead. */ - public addParamDigit(value: number): void { - if (this._rejectDigits) { + public addDigit(value: number): void { + let length; + if (this._rejectDigits + || !(length = this._digitIsSub ? this._subParamsLength : this.length) + || (this._digitIsSub && this._rejectSubDigits) + ) { return; } - const v = this.params[this.length - 1] * 10 + value; - this.params[this.length - 1] = v > MAX_VALUE ? MAX_VALUE : v; - } - /** - * Add a single digit value to current sub parameter. - * This is used by the parser to account digits on a char by char basis. - * Do not use this method directly, consider using `addSubParam` instead. - */ - public addSubParamDigit(value: number): void { - if (!this._subParamsLength || this._rejectDigits || this._rejectSubDigits) { - return; - } - if (this._subParams[this._subParamsLength - 1] === -1) { - this._subParams[this._subParamsLength - 1] = value; - } else { - const v = this._subParams[this._subParamsLength - 1] * 10 + value; - this._subParams[this._subParamsLength - 1] = v > MAX_VALUE ? MAX_VALUE : v; - } + const store = this._digitIsSub ? this._subParams : this.params; + const cur = store[length - 1]; + store[length - 1] = ~cur ? Math.min(cur * 10 + value, MAX_VALUE) : value; } } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index d432800d5..e2fac89fa 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -53,10 +53,8 @@ export interface IParsingState { code: number; // current parser state currentState: ParserState; - // osc string buffer - osc: string; // collect buffer with intermediate characters - collect: string; + collect: number; // params buffer params: IParams; // should abort (default: false) @@ -64,31 +62,83 @@ export interface IParsingState { } /** -* 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. -*/ + * Command handler interfaces. + */ + +/** + * 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 { - hook(collect: string, params: IParams, flag: number): void; + /** + * 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; - unhook(): 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; + +/** + * 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 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; + /** * EscapeSequenceParser interface. @@ -112,31 +162,83 @@ export interface IEscapeSequenceParser extends IDisposable { */ parse(data: Uint32Array, length: number): void; - setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; + /** + * 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(handler: PrintHandlerType): void; clearPrintHandler(): void; - setExecuteHandler(flag: string, callback: () => void): void; + setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void; + clearEscHandler(id: IFunctionIdentifier): void; + setEscHandlerFallback(handler: EscFallbackHandlerType): void; + addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable; + + setExecuteHandler(flag: string, handler: ExecuteHandlerType): void; clearExecuteHandler(flag: string): void; - setExecuteHandlerFallback(callback: (code: number) => void): void; + setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void; - setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void; - clearCsiHandler(flag: string): void; - setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void; - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void; + clearCsiHandler(id: IFunctionIdentifier): void; + setCsiHandlerFallback(callback: CsiFallbackHandlerType): void; + addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable; - setEscHandler(collectAndFlag: string, callback: () => void): void; - clearEscHandler(collectAndFlag: string): void; - setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; + setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; + clearDcsHandler(id: IFunctionIdentifier): void; + setDcsHandlerFallback(handler: DcsFallbackHandlerType): void; + addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; - setOscHandler(ident: number, callback: (data: string) => void): void; + setOscHandler(ident: number, handler: IOscHandler): void; clearOscHandler(ident: number): void; - setOscHandlerFallback(callback: (identifier: number, data: string) => void): void; + setOscHandlerFallback(handler: OscFallbackHandlerType): void; + addOscHandler(ident: number, handler: IOscHandler): IDisposable; - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; - clearDcsHandler(collectAndFlag: string): void; - setDcsHandlerFallback(handler: IDcsHandler): void; - - setErrorHandler(callback: (state: IParsingState) => IParsingState): void; + setErrorHandler(handler: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; } + +/** + * Subparser interfaces. + * The subparsers are instantiated in `EscapeSequenceParser` and + * called during `EscapeSequenceParser.parse`. + */ +export interface ISubParser extends IDisposable { + reset(): 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 ISubParser { + hook(ident: number, params: IParams): 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/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts new file mode 100644 index 000000000..2063a496a --- /dev/null +++ b/src/common/services/CoreMouseService.test.ts @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { CoreMouseService } from 'common/services/CoreMouseService'; +import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; +import { assert } from 'chai'; +import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; + +// needed mock services +const bufferService = new MockBufferService(300, 100); +const coreService = new MockCoreService(); + +function toBytes(s: string | undefined): number[] { + if (!s) { + return []; + } + const res: number[] = []; + for (let i = 0; i < s.length; ++i) { + res.push(s.charCodeAt(i)); + } + return res; +} + +describe('CoreMouseService', () => { + it('init', () => { + const cms = new CoreMouseService(bufferService, coreService); + assert.equal(cms.activeEncoding, 'DEFAULT'); + assert.equal(cms.activeProtocol, 'NONE'); + }); + it('default protocols - NONE, X10, VT200, DRAG, ANY', () => { + const cms = new CoreMouseService(bufferService, coreService); + assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']); + }); + it('default encodings - DEFAULT, UTF8, SGR, URXVT', () => { + const cms = new CoreMouseService(bufferService, coreService); + assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'UTF8', 'SGR', 'URXVT']); + }); + it('protocol/encoding setter, reset', () => { + const cms = new CoreMouseService(bufferService, coreService); + cms.activeEncoding = 'SGR'; + cms.activeProtocol = 'ANY'; + assert.equal(cms.activeEncoding, 'SGR'); + assert.equal(cms.activeProtocol, 'ANY'); + cms.reset(); + assert.equal(cms.activeEncoding, 'DEFAULT'); + assert.equal(cms.activeProtocol, 'NONE'); + assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"'); + assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"'); + }); + it('addEncoding', () => { + const cms = new CoreMouseService(bufferService, coreService); + cms.addEncoding('XYZ', (e: ICoreMouseEvent) => ''); + cms.activeEncoding = 'XYZ'; + assert.equal(cms.activeEncoding, 'XYZ'); + }); + it('addProtocol', () => { + const cms = new CoreMouseService(bufferService, coreService); + cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false }); + cms.activeProtocol = 'XYZ'; + assert.equal(cms.activeProtocol, 'XYZ'); + }); + it('onProtocolChange', () => { + const cms = new CoreMouseService(bufferService, coreService); + const wantedEvents: CoreMouseEventType[] = []; + cms.onProtocolChange(events => wantedEvents.push(events)); + cms.activeProtocol = 'NONE'; + assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]); + cms.activeProtocol = 'ANY'; + assert.deepEqual(wantedEvents, [ + CoreMouseEventType.NONE, + CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE + ]); + }); + describe('triggerMouseEvent', () => { + let cms: CoreMouseService; + let reports: string[]; + beforeEach(() => { + cms = new CoreMouseService(bufferService, coreService); + reports = []; + coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); + }); + it('NONE', () => { + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); + }); + it('X10', () => { + cms.activeProtocol = 'X10'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); + }); + it('VT200', () => { + cms.activeProtocol = 'VT200'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); + }); + it('DRAG', () => { + cms.activeProtocol = 'DRAG'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + }); + it('ANY', () => { + cms.activeProtocol = 'ANY'; + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true); + // should not report in any case + // invalid button + action combinations + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false); + // invalid coords + assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + }); + describe('coords', () => { + it('DEFAULT encoding', () => { + cms.activeProtocol = 'ANY'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + // capped at 95 + if (i < 95) { + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + } else { + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]); + } + } + }); + it('UTF8 encoding', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'UTF8'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + } + }); + it('SGR encoding', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'SGR'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`); + } + }); + it('URXVT', () => { + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'URXVT'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`); + } + }); + }); + it('eventCodes with modifiers (DEFAULT encoding)', () => { + // TODO: implement AUX button tests + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'DEFAULT'; + // all buttons + down + no modifer + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']); + while (reports.pop()) { } + + // all buttons + up + no modifier + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']); + while (reports.pop()) { } + + // all buttons + move + no modifier + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']); + while (reports.pop()) { } + + // button none + move + modifiers + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true); + assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true); + assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']); + while (reports.pop()) { } + }); + }); +}); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts new file mode 100644 index 000000000..0c8465192 --- /dev/null +++ b/src/common/services/CoreMouseService.ts @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; + +/** + * Supported default protocols. + */ +const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { + /** + * NONE + * Events: none + * Modifiers: none + */ + NONE: { + events: CoreMouseEventType.NONE, + restrict: () => false + }, + /** + * X10 + * Events: mousedown + * Modifiers: none + */ + X10: { + events: CoreMouseEventType.DOWN, + restrict: (e: ICoreMouseEvent) => { + // no wheel, no move, no up + if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) { + return false; + } + // no modifiers + e.ctrl = false; + e.alt = false; + e.shift = false; + return true; + } + }, + /** + * VT200 + * Events: mousedown / mouseup / wheel + * Modifiers: all + */ + VT200: { + events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL, + restrict: (e: ICoreMouseEvent) => { + // no move + if (e.action === CoreMouseAction.MOVE) { + return false; + } + return true; + } + }, + /** + * DRAG + * Events: mousedown / mouseup / wheel / mousedrag + * Modifiers: all + */ + DRAG: { + events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG, + restrict: (e: ICoreMouseEvent) => { + // no move without button + if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) { + return false; + } + return true; + } + }, + /** + * ANY + * Events: all mouse related events + * Modifiers: all + */ + ANY: { + events: + CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL + | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE, + restrict: (e: ICoreMouseEvent) => true + } +}; + +const enum Modifiers { + SHIFT = 4, + ALT = 8, + CTRL = 16 +} + +// helper for default encoders to generate the event code. +function eventCode(e: ICoreMouseEvent, isSGR: boolean): number { + let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0); + if (e.button === CoreMouseButton.WHEEL) { + code |= 64; + code |= e.action; + } else { + code |= e.button & 3; + if (e.button & 4) { + code |= 64; + } + if (e.button & 8) { + code |= 128; + } + if (e.action === CoreMouseAction.MOVE) { + code |= CoreMouseAction.MOVE; + } else if (e.action === CoreMouseAction.UP && !isSGR) { + // special case - only SGR can report button on release + // all others have to go with NONE + code |= CoreMouseButton.NONE; + } + } + return code; +} + +const S = String.fromCharCode; + +/** + * Supported default encodings. + */ +const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { + /** + * DEFAULT - CSI M Pb Px Py + * Single byte encoding for coords and event code. + * Can encode values up to 223. The Encoding of higher + * values is not UTF-8 compatible (and currently limited + * to 95 in xterm.js). + */ + DEFAULT: (e: ICoreMouseEvent) => { + let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // FIXME: we are currently limited to ASCII range + params = params.map(v => (v > 127) ? 127 : v); + // FIXED: params = params.map(v => (v > 255) ? 0 : value); + return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; + }, + /** + * UTF8 - CSI M Pb Px Py + * Same as DEFAULT, but with optional 2-byte UTF8 + * encoding for values > 223 (can encode up to 2015). + */ + UTF8: (e: ICoreMouseEvent) => { + let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // limit to 2-byte UTF8 + params = params.map(v => (v > 2047) ? 0 : v); + return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; + }, + /** + * SGR - CSI < Pb ; Px ; Py M|m + * No encoding limitation. + * Can report button on release and works with a well formed sequence. + */ + SGR: (e: ICoreMouseEvent) => { + const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M'; + return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`; + }, + /** + * URXVT - CSI Pb ; Px ; Py M + * Same button encoding as default, decimal encoding for coords. + * Ambiguity with other sequences, should not be used. + */ + URXVT: (e: ICoreMouseEvent) => { + return `\x1b[${eventCode(e, false) + 32};${e.col};${e.row}M`; + } +}; + +/** + * CoreMouseService + * + * Provides mouse tracking reports with different protocols and encodings. + * - protocols: NONE (default), X10, VT200, DRAG, ANY + * - encodings: DEFAULT, SGR, UTF8, URXVT + * + * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`. + * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`. + * Switching a protocol will send a notification event `onProtocolChange` + * with a list of needed events to track. + * + * The service handles the mouse tracking state and decides whether to send + * a tracking report to the backend based on protocol and encoding limitations. + * To send a mouse event call `triggerMouseEvent`. + */ +export class CoreMouseService implements ICoreMouseService { + private _protocols: {[name: string]: ICoreMouseProtocol} = {}; + private _encodings: {[name: string]: CoreMouseEncoding} = {}; + private _activeProtocol: string = ''; + private _activeEncoding: string = ''; + private _onProtocolChange = new EventEmitter(); + private _lastEvent: ICoreMouseEvent | null = null; + + constructor( + @IBufferService private readonly _bufferService: IBufferService, + @ICoreService private readonly _coreService: ICoreService + ) { + // register default protocols and encodings + Object.keys(DEFAULT_PROTOCOLS).forEach(name => this.addProtocol(name, DEFAULT_PROTOCOLS[name])); + Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name])); + // call reset to set defaults + this.reset(); + } + + public addProtocol(name: string, protocol: ICoreMouseProtocol): void { + this._protocols[name] = protocol; + } + + public addEncoding(name: string, encoding: CoreMouseEncoding): void { + this._encodings[name] = encoding; + } + + public get activeProtocol(): string { + return this._activeProtocol; + } + + public set activeProtocol(name: string) { + if (!this._protocols[name]) { + throw new Error(`unknown protocol "${name}"`); + } + this._activeProtocol = name; + this._onProtocolChange.fire(this._protocols[name].events); + } + + public get activeEncoding(): string { + return this._activeEncoding; + } + + public set activeEncoding(name: string) { + if (!this._encodings[name]) { + throw new Error(`unknown encoding "${name}"`); + } + this._activeEncoding = name; + } + + public reset(): void { + this.activeProtocol = 'NONE'; + this.activeEncoding = 'DEFAULT'; + this._lastEvent = null; + } + + /** + * Event to announce changes in mouse tracking. + */ + public get onProtocolChange(): IEvent { + return this._onProtocolChange.event; + } + + /** + * Triggers a mouse event to be sent. + * + * Returns true if the event passed all protocol restrictions and a report + * was sent, otherwise false. The return value may be used to decide whether + * the default event action in the bowser component should be omitted. + * + * Note: The method will change values of the given event object + * to fullfill protocol and encoding restrictions. + */ + public triggerMouseEvent(e: ICoreMouseEvent): boolean { + // range check for col/row + if (e.col < 0 || e.col >= this._bufferService.cols + || e.row < 0 || e.row >= this._bufferService.rows) { + return false; + } + + // filter nonsense combinations of button + action + if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) { + return false; + } + if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) { + return false; + } + if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) { + return false; + } + + // report 1-based coords + e.col++; + e.row++; + + // debounce move at grid level + if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) { + return false; + } + + // apply protocol restrictions + if (!this._protocols[this._activeProtocol].restrict(e)) { + return false; + } + + // encode report and send + const report = this._encodings[this._activeEncoding](e); + this._coreService.triggerDataEvent(report, true); + + this._lastEvent = e; + + return true; + } + + public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { + return { + DOWN: !!(events & CoreMouseEventType.DOWN), + UP: !!(events & CoreMouseEventType.UP), + DRAG: !!(events & CoreMouseEventType.DRAG), + MOVE: !!(events & CoreMouseEventType.MOVE), + WHEEL: !!(events & CoreMouseEventType.WHEEL) + }; + } + + private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean { + if (e1.col !== e2.col) return false; + if (e1.row !== e2.row) return false; + if (e1.button !== e2.button) return false; + if (e1.action !== e2.action) return false; + if (e1.ctrl !== e2.ctrl) return false; + if (e1.alt !== e2.alt) return false; + if (e1.shift !== e2.shift) return false; + return true; + } +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 8f4eb39ee..1df59f84b 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -23,6 +23,37 @@ export interface IBufferService { reset(): void; } +export const ICoreMouseService = createDecorator('CoreMouseService'); +export interface ICoreMouseService { + activeProtocol: string; + activeEncoding: string; + addProtocol(name: string, protocol: ICoreMouseProtocol): void; + addEncoding(name: string, encoding: CoreMouseEncoding): void; + reset(): void; + + /** + * Triggers a mouse event to be sent. + * + * Returns true if the event passed all protocol restrictions and a report + * was sent, otherwise false. The return value may be used to decide whether + * the default event action in the bowser component should be omitted. + * + * Note: The method will change values of the given event object + * to fullfill protocol and encoding restrictions. + */ + triggerMouseEvent(event: ICoreMouseEvent): boolean; + + /** + * Event to announce changes in mouse tracking. + */ + onProtocolChange: IEvent; + + /** + * Human readable version of mouse events. + */ + explainEvents(events: CoreMouseEventType): {[event: string]: boolean}; +} + export const ICoreService = createDecorator('CoreService'); export interface ICoreService { serviceBrand: any; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 5d270ca71..6d6cedcac 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'; @@ -16,6 +16,7 @@ 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,12 +64,6 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable { - return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect)); - } - public 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); } @@ -133,6 +134,9 @@ export class Terminal implements ITerminalApi { public writeUtf8(data: Uint8Array): void { this._core.writeUtf8(data); } + public paste(data: string): void { + this._core.paste(data); + } public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; public getOption(key: 'colors'): string[]; @@ -164,9 +168,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); } @@ -220,3 +221,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/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index dfd16ddb3..9ca0c943d 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -334,23 +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('m', (params, collect) => { - _customCsiHandlerParams.push(params); - return false; - }, ''); - `); - await page.evaluate(` - window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - `); - assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); - }); - }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index 266edff7e..90c18f753 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -74,10 +74,6 @@ async function mouseMove(col: number, row: number): Promise { const [xPixels, yPixels] = await cellPos(col, row); return await page.mouse.move(xPixels, yPixels); } -async function mouseClick(col: number, row: number): Promise { - const [xPixels, yPixels] = await cellPos(col, row); - return await page.mouse.click(xPixels, yPixels); -} async function mouseDown(button: 'left' | 'right' | 'middle' | undefined): Promise { return await page.mouse.down({button}); } @@ -86,15 +82,25 @@ async function mouseUp(button: 'left' | 'right' | 'middle' | undefined): Promise } async function wheelUp(): Promise { const self = (page.mouse as any); - return await page.evaluate(` - window.term.element.dispatchEvent(new WheelEvent('wheel', {clientX: ${self._x}, clientY: ${self._y}, deltaX: 0, deltaY: -10, modifiers: ${self._keyboard._modifiers}})); - `); + return await self._client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: self._x, + y: self._y, + deltaX: 0, + deltaY: -10, + modifiers: self._keyboard._modifiers + }); } async function wheelDown(): Promise { const self = (page.mouse as any); - return await page.evaluate(` - window.term.element.dispatchEvent(new WheelEvent('wheel', {clientX: ${self._x}, clientY: ${self._y}, deltaX: 0, deltaY: 10, modifiers: ${self._keyboard._modifiers}})); - `); + return await self._client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: self._x, + y: self._y, + deltaX: 0, + deltaY: 10, + modifiers: self._keyboard._modifiers + }); } // button definitions @@ -134,7 +140,7 @@ function evalButtonCode(code: number): any { button |= 8; } if (code & 64) { - button |= 4 + button |= 4; } let actionS = 'press'; let buttonS = reverseButtons[button]; @@ -190,7 +196,7 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number; case 'URXVT': sReport = report.slice(2, -1); [buttonCode, col, row] = sReport.split(';').map(el => parseInt(el)); - return {state: evalButtonCode(buttonCode - 32), row: --row, col: --col}; // FIXME: remove -1 here when fixed! + return {state: evalButtonCode(buttonCode - 32), row, col}; default: return { state: evalButtonCode(report.charCodeAt(3) - 32), @@ -204,7 +210,7 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number; * Mouse tracking tests. */ describe('Mouse Tracking Tests', function(): void { - this.timeout(30000); + this.timeout(60000); before(async function(): Promise { browser = await puppeteer.launch({ @@ -348,25 +354,24 @@ describe('Mouse Tracking Tests', function(): void { {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); } - /* + // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - */ }); - it('UTF8 encoding', async () => { + it.skip('UTF8 encoding', async () => { const encoding = 'UTF8'; await resetMouseModes(); await mouseMove(0, 0); @@ -467,23 +472,22 @@ describe('Mouse Tracking Tests', function(): void { {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); } - /* + // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - */ }); it('SGR encoding', async () => { const encoding = 'SGR'; @@ -586,25 +590,24 @@ describe('Mouse Tracking Tests', function(): void { {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); } - /* + // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - */ }); - it('URXVT encoding', async () => { + it.skip('URXVT encoding', async () => { // bug: always reports +1 for row/col (temp. fixed in parseReport to pass tests) const encoding = 'URXVT'; await resetMouseModes(); @@ -706,23 +709,22 @@ describe('Mouse Tracking Tests', function(): void { {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); } - /* + // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); - */ }); }); describe('DECSET 1000 (VT200 mouse)', () => { @@ -740,17 +742,16 @@ describe('Mouse Tracking Tests', function(): void { await page.evaluate(`window.term.write('\x1b[?1000h');`); // test at 0,0 - // bug: release is fired immediately - expected: only press event await mouseDown('left'); assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button - // bug: release already fired thus no event here - expected: release event await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); // mousemove should not report await mouseMove(50, 10); @@ -775,7 +776,6 @@ describe('Mouse Tracking Tests', function(): void { // button press/move/release tests // left button - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('left'); @@ -783,7 +783,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) @@ -795,7 +795,6 @@ describe('Mouse Tracking Tests', function(): void { // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('right'); @@ -803,7 +802,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('right'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // wheel @@ -816,28 +815,21 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported, release with wrong coords await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -847,13 +839,9 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT @@ -869,56 +857,53 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - selection manager? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); - it('UTF8 encoding', async () => { + it.skip('UTF8 encoding', async () => { const encoding = 'UTF8'; await resetMouseModes(); await mouseMove(0, 0); await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1005h');`); // test at 0,0 - // bug: release is fired immediately - expected: only press event await mouseDown('left'); assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button - // bug: release already fired thus no event here - expected: release event await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); // mousemove should not report await mouseMove(50, 10); @@ -941,7 +926,6 @@ describe('Mouse Tracking Tests', function(): void { // button press/move/release tests // left button - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('left'); @@ -949,7 +933,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) @@ -961,7 +945,6 @@ describe('Mouse Tracking Tests', function(): void { // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('right'); @@ -969,7 +952,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('right'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // wheel @@ -982,28 +965,21 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported, release with wrong coords await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -1013,18 +989,13 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -1035,37 +1006,35 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - selection manager? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); it('SGR encoding', async () => { const encoding = 'SGR'; @@ -1074,17 +1043,16 @@ describe('Mouse Tracking Tests', function(): void { await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1006h');`); // test at 0,0 - // bug: release is fired immediately - expected: only press event await mouseDown('left'); assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report - // bug: release already fired thus no event here - expected: release event await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); // mousemove should not report await mouseMove(50, 10); @@ -1107,7 +1075,6 @@ describe('Mouse Tracking Tests', function(): void { // button press/move/release tests // left button - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('left'); @@ -1115,7 +1082,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) @@ -1127,8 +1094,6 @@ describe('Mouse Tracking Tests', function(): void { // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 - // bug: release reports wrong button await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('right'); @@ -1136,7 +1101,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('right'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: 'right', modifier: {control: false, shift: false, meta: false}}} ]); // wheel @@ -1149,28 +1114,21 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported, release with wrong coords await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -1180,18 +1138,13 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -1202,56 +1155,53 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - selection manager? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); - it('URXVT encoding', async () => { + it.skip('URXVT encoding', async () => { const encoding = 'URXVT'; await resetMouseModes(); await mouseMove(0, 0); await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1015h');`); // test at 0,0 - // bug: release is fired immediately - expected: only press event await mouseDown('left'); assert.deepEqual(await getReports(encoding), [ - {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} ]); // mouseup should report, encoding cannot report released button - // bug: release already fired thus no event here - expected: release event await mouseUp('left'); - assert.deepEqual(await getReports(encoding), []); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); // mousemove should not report await mouseMove(50, 10); @@ -1274,7 +1224,6 @@ describe('Mouse Tracking Tests', function(): void { // button press/move/release tests // left button - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('left'); @@ -1282,7 +1231,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('left'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) @@ -1294,7 +1243,6 @@ describe('Mouse Tracking Tests', function(): void { // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) - // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('right'); @@ -1302,7 +1250,7 @@ describe('Mouse Tracking Tests', function(): void { await mouseUp('right'); assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // wheel @@ -1315,28 +1263,21 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported, release with wrong coords await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -1346,18 +1287,13 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -1368,42 +1304,39 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working - selection manager? await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); }); describe('DECSET 1002 (xterm with drag)', () => { /** - * VT200 protocol: * - press and release events * - wheel up/down * - move only on press (drag) @@ -1492,30 +1425,22 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -1525,20 +1450,14 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release/drag caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -1549,42 +1468,39 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - // bug: completely messed up - wrong modifier, only partially reported assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); - it('UTF8 encoding', async () => { + it.skip('UTF8 encoding', async () => { const encoding = 'UTF8'; await resetMouseModes(); await mouseMove(0, 0); @@ -1666,30 +1582,22 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -1699,20 +1607,14 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release/drag caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -1723,40 +1625,38 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { // bug: completely messed up - wrong modifier, only partially reported assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); it('SGR encoding', async () => { const encoding = 'SGR'; @@ -1818,7 +1718,6 @@ describe('Mouse Tracking Tests', function(): void { // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) - // bug: release reports wrong button await mouseMove(43, 24); await getReports(encoding); // clear reports await mouseDown('right'); @@ -1827,7 +1726,7 @@ describe('Mouse Tracking Tests', function(): void { assert.deepEqual(await getReports(encoding), [ {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'release', button: 'right', modifier: {control: false, shift: false, meta: false}}} ]); // wheel @@ -1840,30 +1739,22 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -1873,20 +1764,14 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release/drag caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -1897,42 +1782,39 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - // bug: completely messed up - wrong modifier, only partially reported assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers // bug: this is totally broken with wrong coords and messed up modifiers await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); - it('URXVT encoding', async () => { + it.skip('URXVT encoding', async () => { const encoding = 'URXVT'; await resetMouseModes(); await mouseMove(0, 0); @@ -2013,30 +1895,22 @@ describe('Mouse Tracking Tests', function(): void { // modifiers // CTRL - // bug: totally broken - reports no modifier, reports wrong button and action - // after fix: removed faulty reports below and uncomment lines await mouseMove(43, 24); await getReports(encoding); // clear reports - // await page.keyboard.down('Control'); + await page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); - // await page.keyboard.up('Control'); + await page.keyboard.up('Control'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} ]); // ALT - // bug: no modifier reported await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Alt'); @@ -2046,20 +1920,14 @@ describe('Mouse Tracking Tests', function(): void { await wheelDown(); await page.keyboard.up('Alt'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} - // expected - // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, - // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} ]); // SHIFT // note: press/release/drag caught by selection manager - // bug: modifier not reported for passed events await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Shift'); // defaults to ShiftLeft @@ -2070,49 +1938,692 @@ describe('Mouse Tracking Tests', function(): void { await page.keyboard.up('Shift'); if (noShift) { assert.deepEqual(await getReports(encoding), [ - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } else { - // bug: completely messed up - wrong modifier, only partially reported assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} ]); } - /* // all modifiers - // bug: this is totally broken with wrong coords and messed up modifiers + // bug: Shift not working await mouseMove(43, 24); await getReports(encoding); // clear reports await page.keyboard.down('Control'); await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + // await page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); await wheelDown(); await page.keyboard.up('Control'); await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); + // await page.keyboard.up('Shift'); assert.deepEqual(await getReports(encoding), [ - {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}}, - {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}} + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} ]); - */ }); }); describe('DECSET 1003 (xterm any event)', () => { /** - * VT200 protocol: * - all events (press, release, wheel, move) * - all modifiers */ - // bug: currently same reports as 1002, FIXME: implement tests once fixed + it('default encoding', async () => { + const encoding = 'DEFAULT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1003h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 95, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // CTRL + await page.keyboard.down('Control'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + ]); + + // ALT + await page.keyboard.down('Alt'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Alt'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + ]); + + // SHIFT + // note: press/release/drag caught by selection manager + await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Shift'); + if (noShift) { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } else { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } + + // all modifiers + // bug: Shift not working + await page.keyboard.down('Control'); + await page.keyboard.down('Alt'); + // await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + await page.keyboard.up('Alt'); + // await page.keyboard.up('Shift'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} + ]); + }); + it.skip('UTF8 encoding', async () => { + const encoding = 'UTF8'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1003h\x1b[?1005h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // CTRL + await page.keyboard.down('Control'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + ]); + + // ALT + await page.keyboard.down('Alt'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Alt'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + ]); + + // SHIFT + // note: press/release/drag caught by selection manager + await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Shift'); + if (noShift) { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } else { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } + + // all modifiers + // bug: Shift not working + await page.keyboard.down('Control'); + await page.keyboard.down('Alt'); + // await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + await page.keyboard.up('Alt'); + // await page.keyboard.up('Shift'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} + ]); + }); + it('SGR encoding', async () => { + const encoding = 'SGR'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1003h\x1b[?1006h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'right', modifier: {control: false, shift: false, meta: false}}} + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // CTRL + await page.keyboard.down('Control'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + ]); + + // ALT + await page.keyboard.down('Alt'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Alt'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + ]); + + // SHIFT + // note: press/release/drag caught by selection manager + await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Shift'); + if (noShift) { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } else { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } + + // all modifiers + // bug: Shift not working + await page.keyboard.down('Control'); + await page.keyboard.down('Alt'); + // await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + await page.keyboard.up('Alt'); + // await page.keyboard.up('Shift'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} + ]); + }); + it.skip('URXVT encoding', async () => { + const encoding = 'URXVT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1003h\x1b[?1015h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // CTRL + await page.keyboard.down('Control'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}} + ]); + + // ALT + await page.keyboard.down('Alt'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Alt'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}} + ]); + + // SHIFT + // note: press/release/drag caught by selection manager + await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Shift'); + if (noShift) { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } else { + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}} + ]); + } + + // all modifiers + // bug: Shift not working + await page.keyboard.down('Control'); + await page.keyboard.down('Alt'); + // await page.keyboard.down('Shift'); + await mouseMove(43, 24); + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + await wheelDown(); + await page.keyboard.up('Control'); + await page.keyboard.up('Alt'); + // await page.keyboard.up('Shift'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'move', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}}, + {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}} + ]); + }); }); - // TODO: move tests with several buttons pressed + /** + * move tests with multiple buttons pressed: + * currently not possible due to a limitation of the puppeteer mouse interface + * (saves only the last one pressed) + */ }); diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts new file mode 100644 index 000000000..28f7d8b34 --- /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/api/Terminal.api.ts b/test/api/Terminal.api.ts index e536e88af..b2157b1ef 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -76,6 +76,22 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); }); + it('paste', async function(): Promise { + await openTerminal(); + await page.evaluate(` + window.calls = []; + window.term.onData(e => calls.push(e)); + window.term.paste('foo'); + window.term.paste('\\r\\nfoo\\nbar\\r'); + window.term.write('\\x1b[?2004h'); + // TODO: Use promise/callback for write when we support that + // Force sync write + window.term._core._innerWrite(); + window.term.paste('foo'); + `); + assert.deepEqual(await page.evaluate(`window.calls`), ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~']); + }); + it('clear', async function(): Promise { await openTerminal({ rows: 5 }); await page.evaluate(` diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index b9a6b0df6..fa22dc58f 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -7,6 +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 { OscHandler } from 'common/parser/OscParser'; function toUtf32(s: string): Uint32Array { @@ -18,7 +19,7 @@ function toUtf32(s: string): Uint32Array { } class DcsHandler implements IDcsHandler { - hook(collect: string, params: IParams, flag: number) : void {} + hook(params: IParams) : void {} put(data: Uint32Array, start: number, end: number) : void {} unhook() :void {} } @@ -31,42 +32,42 @@ perfContext('Parser throughput - 50MB data', () => { beforeEach(() => { parser = new EscapeSequenceParser(); parser.setPrintHandler((data, start, end) => {}); - parser.setCsiHandler('@', (params, collect) => {}); - parser.setCsiHandler('A', (params, collect) => {}); - parser.setCsiHandler('B', (params, collect) => {}); - parser.setCsiHandler('C', (params, collect) => {}); - parser.setCsiHandler('D', (params, collect) => {}); - parser.setCsiHandler('E', (params, collect) => {}); - parser.setCsiHandler('F', (params, collect) => {}); - parser.setCsiHandler('G', (params, collect) => {}); - parser.setCsiHandler('H', (params, collect) => {}); - parser.setCsiHandler('I', (params, collect) => {}); - parser.setCsiHandler('J', (params, collect) => {}); - parser.setCsiHandler('K', (params, collect) => {}); - parser.setCsiHandler('L', (params, collect) => {}); - parser.setCsiHandler('M', (params, collect) => {}); - parser.setCsiHandler('P', (params, collect) => {}); - parser.setCsiHandler('S', (params, collect) => {}); - parser.setCsiHandler('T', (params, collect) => {}); - parser.setCsiHandler('X', (params, collect) => {}); - parser.setCsiHandler('Z', (params, collect) => {}); - parser.setCsiHandler('`', (params, collect) => {}); - parser.setCsiHandler('a', (params, collect) => {}); - parser.setCsiHandler('b', (params, collect) => {}); - parser.setCsiHandler('c', (params, collect) => {}); - parser.setCsiHandler('d', (params, collect) => {}); - parser.setCsiHandler('e', (params, collect) => {}); - parser.setCsiHandler('f', (params, collect) => {}); - parser.setCsiHandler('g', (params, collect) => {}); - parser.setCsiHandler('h', (params, collect) => {}); - parser.setCsiHandler('l', (params, collect) => {}); - parser.setCsiHandler('m', (params, collect) => {}); - parser.setCsiHandler('n', (params, collect) => {}); - parser.setCsiHandler('p', (params, collect) => {}); - parser.setCsiHandler('q', (params, collect) => {}); - parser.setCsiHandler('r', (params, collect) => {}); - parser.setCsiHandler('s', (params, collect) => {}); - parser.setCsiHandler('u', (params, collect) => {}); + parser.setCsiHandler({final: '@'}, params => {}); + parser.setCsiHandler({final: 'A'}, params => {}); + parser.setCsiHandler({final: 'B'}, params => {}); + parser.setCsiHandler({final: 'C'}, params => {}); + parser.setCsiHandler({final: 'D'}, params => {}); + parser.setCsiHandler({final: 'E'}, params => {}); + parser.setCsiHandler({final: 'F'}, params => {}); + parser.setCsiHandler({final: 'G'}, params => {}); + parser.setCsiHandler({final: 'H'}, params => {}); + parser.setCsiHandler({final: 'I'}, params => {}); + parser.setCsiHandler({final: 'J'}, params => {}); + parser.setCsiHandler({final: 'K'}, params => {}); + parser.setCsiHandler({final: 'L'}, params => {}); + parser.setCsiHandler({final: 'M'}, params => {}); + parser.setCsiHandler({final: 'P'}, params => {}); + parser.setCsiHandler({final: 'S'}, params => {}); + parser.setCsiHandler({final: 'T'}, params => {}); + parser.setCsiHandler({final: 'X'}, params => {}); + parser.setCsiHandler({final: 'Z'}, params => {}); + parser.setCsiHandler({final: '`'}, params => {}); + parser.setCsiHandler({final: 'a'}, params => {}); + parser.setCsiHandler({final: 'b'}, params => {}); + parser.setCsiHandler({final: 'c'}, params => {}); + parser.setCsiHandler({final: 'd'}, params => {}); + parser.setCsiHandler({final: 'e'}, params => {}); + parser.setCsiHandler({final: 'f'}, params => {}); + parser.setCsiHandler({final: 'g'}, params => {}); + parser.setCsiHandler({final: 'h'}, params => {}); + parser.setCsiHandler({final: 'l'}, params => {}); + parser.setCsiHandler({final: 'm'}, params => {}); + parser.setCsiHandler({final: 'n'}, params => {}); + parser.setCsiHandler({final: 'p'}, params => {}); + parser.setCsiHandler({final: 'q'}, params => {}); + parser.setCsiHandler({final: 'r'}, params => {}); + parser.setCsiHandler({final: 's'}, params => {}); + parser.setCsiHandler({final: 'u'}, params => {}); parser.setExecuteHandler(C0.BEL, () => {}); parser.setExecuteHandler(C0.LF, () => {}); parser.setExecuteHandler(C0.VT, () => {}); @@ -79,25 +80,25 @@ perfContext('Parser throughput - 50MB data', () => { parser.setExecuteHandler(C1.IND, () => {}); parser.setExecuteHandler(C1.NEL, () => {}); parser.setExecuteHandler(C1.HTS, () => {}); - parser.setOscHandler(0, (data) => {}); - parser.setOscHandler(2, (data) => {}); - parser.setEscHandler('7', () => {}); - parser.setEscHandler('8', () => {}); - parser.setEscHandler('D', () => {}); - parser.setEscHandler('E', () => {}); - parser.setEscHandler('H', () => {}); - parser.setEscHandler('M', () => {}); - parser.setEscHandler('=', () => {}); - parser.setEscHandler('>', () => {}); - parser.setEscHandler('c', () => {}); - parser.setEscHandler('n', () => {}); - parser.setEscHandler('o', () => {}); - parser.setEscHandler('|', () => {}); - parser.setEscHandler('}', () => {}); - parser.setEscHandler('~', () => {}); - parser.setEscHandler('%@', () => {}); - parser.setEscHandler('%G', () => {}); - parser.setDcsHandler('q', new DcsHandler()); + parser.setOscHandler(0, new OscHandler((data) => {})); + parser.setOscHandler(2, new OscHandler((data) => {})); + parser.setEscHandler({final: '7'}, () => {}); + parser.setEscHandler({final: '8'}, () => {}); + parser.setEscHandler({final: 'D'}, () => {}); + parser.setEscHandler({final: 'E'}, () => {}); + parser.setEscHandler({final: 'H'}, () => {}); + parser.setEscHandler({final: 'M'}, () => {}); + parser.setEscHandler({final: '='}, () => {}); + parser.setEscHandler({final: '>'}, () => {}); + parser.setEscHandler({final: 'c'}, () => {}); + parser.setEscHandler({final: 'n'}, () => {}); + parser.setEscHandler({final: 'o'}, () => {}); + parser.setEscHandler({final: '|'}, () => {}); + parser.setEscHandler({final: '}'}, () => {}); + parser.setEscHandler({final: '~'}, () => {}); + parser.setEscHandler({intermediates: '%', final: '@'}, () => {}); + parser.setEscHandler({intermediates: '%', final: 'G'}, () => {}); + parser.setDcsHandler({final: 'q'}, new DcsHandler()); }); perfContext('PRINT - a', () => { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 9ea85d07e..6a2911254 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -217,47 +217,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; } /** @@ -396,6 +396,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. */ @@ -510,32 +516,6 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - /** - * (EXPERIMENTAL) Adds a handler for CSI escape sequences. - * @param flag The flag should be one-character string, which specifies the - * final character (e.g "m" for SGR) of the CSI sequence. - * @param callback The function to handle the escape sequence. The callback - * is called with the numerical params, as well as the special characters - * (e.g. "$" for DECSCPP). If the sequence has subparams the array will - * contain subarrays with their numercial values. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; - - /** - * (EXPERIMENTAL) Adds a handler for OSC escape sequences. - * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the escape sequence. 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. @@ -699,6 +679,12 @@ declare module 'xterm' { */ writeUtf8(data: Uint8Array): void; + /** + * Writes text to the terminal, performing the necessary transformations for pasted text. + * @param data The text to write to the terminal. + */ + paste(data: string): void; + /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -814,18 +800,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; @@ -961,4 +939,117 @@ declare module 'xterm' { */ readonly width: number; } + + /** + * (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: + * - 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. + */ + 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; + } + + /** + * (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; + } } diff --git a/yarn.lock b/yarn.lock index a67ede71d..7b987a00d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -352,10 +352,10 @@ acorn@^5.0.0, acorn@^5.3.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" integrity sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ== -acorn@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3" - integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw== +acorn@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" + integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== agent-base@^4.1.0: version "4.2.1" @@ -369,10 +369,10 @@ ajv-errors@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== -ajv-keywords@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" - integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== ajv@^5.1.0: version "5.5.2" @@ -384,15 +384,15 @@ ajv@^5.1.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz#678495f9b82f7cca6be248dd92f59bff5e1f4360" - integrity sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA== +ajv@^6.1.0, ajv@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" - uri-js "^4.2.1" + uri-js "^4.2.2" ansi-colors@3.2.3: version "3.2.3" @@ -509,10 +509,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= assert@^1.1.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" - integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== dependencies: + object-assign "^4.1.1" util "0.10.3" assertion-error@^1.0.1: @@ -525,10 +526,10 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -async-each@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - integrity sha1-GdOGodntxufByF04iu28xW0zYC0= +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== async-limiter@^1.0.0, async-limiter@~1.0.0: version "1.0.0" @@ -555,9 +556,9 @@ asynckit@^0.4.0: integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= atob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a" - integrity sha1-ri1acpR38onWDdf5amMUoi3Wwio= + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== aws-sign2@~0.7.0: version "0.7.0" @@ -575,9 +576,9 @@ balanced-match@^1.0.0: integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= base64-js@^1.0.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" - integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== base@^0.11.1: version "0.11.2" @@ -610,9 +611,9 @@ big.js@^5.2.2: integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" - integrity sha1-RqoXUftqL5PuXmibsQh9SxTGwgU= + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== bluebird@^3.5.5: version "3.5.5" @@ -648,7 +649,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^2.3.0, braces@^2.3.1: +braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== @@ -746,9 +747,9 @@ browserify-zlib@^0.2.0: pako "~1.0.5" buffer-from@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" - integrity sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== buffer-xor@^1.0.3: version "1.0.3" @@ -779,16 +780,17 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@^11.3.2: - version "11.3.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc" - integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA== +cacache@^12.0.2: + version "12.0.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" + integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw== dependencies: bluebird "^3.5.5" chownr "^1.1.1" figgy-pudding "^3.5.1" glob "^7.1.4" graceful-fs "^4.1.15" + infer-owner "^1.0.3" lru-cache "^5.1.1" mississippi "^3.0.0" mkdirp "^0.5.1" @@ -862,29 +864,23 @@ chardet@^0.7.0: integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== chokidar@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" - integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ== + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== dependencies: anymatch "^2.0.0" - async-each "^1.0.0" - braces "^2.3.0" + async-each "^1.0.1" + braces "^2.3.2" glob-parent "^3.1.0" - inherits "^2.0.1" + inherits "^2.0.3" is-binary-path "^1.0.0" is-glob "^4.0.0" - lodash.debounce "^4.0.8" - normalize-path "^2.1.1" + normalize-path "^3.0.0" path-is-absolute "^1.0.0" - readdirp "^2.0.0" - upath "^1.0.5" + readdirp "^2.2.1" + upath "^1.1.1" optionalDependencies: - fsevents "^1.2.2" - -chownr@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" - integrity sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE= + fsevents "^1.2.7" chownr@^1.1.1: version "1.1.2" @@ -910,10 +906,10 @@ chrome-timeline@0.0.12: simple-git "^1.113.0" winston "^3.2.1" -chrome-trace-event@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" - integrity sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A== +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== dependencies: tslib "^1.9.0" @@ -1090,9 +1086,9 @@ complex.js@2.0.11: integrity sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw== component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== concat-map@0.0.1: version "0.0.1" @@ -1250,10 +1246,10 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" -cyclist@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" - integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= +cyclist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" + integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= dashdash@^1.12.0: version "1.14.1" @@ -1276,14 +1272,14 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@3.2.6: +debug@3.2.6, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -1456,9 +1452,9 @@ domexception@^1.0.0: webidl-conversions "^4.0.2" duplexify@^3.4.2, duplexify@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410" - integrity sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ== + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== dependencies: end-of-stream "^1.0.0" inherits "^2.0.1" @@ -1478,9 +1474,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= elliptic@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" - integrity sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8= + version "6.5.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b" + integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -1600,10 +1596,10 @@ escodegen@^1.9.0: optionalDependencies: source-map "~0.6.1" -eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -1625,7 +1621,12 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= @@ -1640,10 +1641,10 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -events@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" @@ -1877,7 +1878,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^2.0.0: +find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -1908,12 +1909,12 @@ flat@^4.1.0: is-buffer "~2.0.3" flush-write-stream@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" - integrity sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw== + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== dependencies: - inherits "^2.0.1" - readable-stream "^2.0.4" + inherits "^2.0.3" + readable-stream "^2.3.6" for-in@^1.0.2: version "1.0.2" @@ -1965,9 +1966,9 @@ from2@^2.1.0: readable-stream "^2.0.0" fs-minipass@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" - integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== dependencies: minipass "^2.2.1" @@ -1986,13 +1987,13 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" - integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg== +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + nan "^2.12.1" + node-pre-gyp "^0.12.0" function-bind@^1.1.1: version "1.1.1" @@ -2096,15 +2097,10 @@ global-modules-path@^2.1.0: resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.0.tgz#b0e2bac6beac39745f7db5c59d26a36a0b94f7dc" integrity sha512-HchvMJNYh9dGSCy8pOQ2O8u/hoXaL+0XhnrwH0RyLiSXMMTl9W3N6KUU73+JFOg5PGjtzl6VZzUQsnrpm7Szag== -graceful-fs@^4.1.15: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" - integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== - -graceful-fs@^4.1.2: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" + integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== growl@1.10.5: version "1.10.5" @@ -2186,9 +2182,9 @@ hash-base@^3.0.0: safe-buffer "^5.0.1" hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.5" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812" - integrity sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA== + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== dependencies: inherits "^2.0.3" minimalistic-assert "^1.0.1" @@ -2263,24 +2259,17 @@ iconv-lite@0.4.19: resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.4.4: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - ieee754@^1.1.4: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== iferr@^0.1.5: version "0.1.5" @@ -2307,10 +2296,10 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= +infer-owner@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== inflight@^1.0.4: version "1.0.6" @@ -2320,20 +2309,20 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= -inherits@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@~1.3.0: version "1.3.5" @@ -2499,9 +2488,9 @@ is-glob@^3.1.0: is-extglob "^2.1.0" is-glob@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" - integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A= + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" @@ -2517,7 +2506,7 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -2750,12 +2739,12 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -loader-runner@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" - integrity sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI= +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= @@ -2764,7 +2753,7 @@ loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@^1.2.3: +loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -2789,11 +2778,6 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -2884,12 +2868,13 @@ mathjs@^5.10.3: typed-function "1.1.0" md5.js@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" - integrity sha1-6b296UogpawYsENA/Fdk1bCdkB0= + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== dependencies: hash-base "^3.0.0" inherits "^2.0.1" + safe-buffer "^5.1.2" media-typer@0.3.0: version "0.3.0" @@ -2912,7 +2897,7 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memory-fs@^0.4.0, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -2930,7 +2915,7 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -3019,7 +3004,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -3036,18 +3021,18 @@ minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minipass@^2.2.1, minipass@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" - integrity sha512-/jAn9/tEX4gnpyRATxgHEOV6xbcyxgT7iUnxo9Y3+OB0zX00TgKIv/2FZCf5brBbICcwbLqVv2ImjvWWrQMSYw== +minipass@^2.2.1, minipass@^2.3.5: + version "2.5.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.5.0.tgz#dddb1d001976978158a05badfcbef4a771612857" + integrity sha512-9FwMVYhn6ERvMR8XFdOavRz4QK/VJV8elU1x50vYexf9lslDcWe/f4HBRxCPd185ekRSjU6CfYyJCECa/CQy7Q== dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" -minizlib@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" - integrity sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA== +minizlib@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== dependencies: minipass "^2.2.1" @@ -3068,14 +3053,14 @@ mississippi@^3.0.0: through2 "^2.0.0" mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: +mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -3138,11 +3123,16 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@2.10.0, nan@^2.9.2: +nan@2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -3161,11 +3151,11 @@ nanomatch@^1.2.9: to-regex "^3.0.1" needle@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" - integrity sha512-t/ZswCM9JTWjAdXS9VpvqhI2Ct2sL2MdY4fUXqGJaGBk13ge99ObqRksRTbBE56K+wxUXwwfZYOuZHifFW9q+Q== + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== dependencies: - debug "^2.1.2" + debug "^3.2.6" iconv-lite "^0.4.4" sax "^1.2.4" @@ -3174,10 +3164,10 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee" - integrity sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA== +neo-async@^2.5.0, neo-async@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== nice-try@^1.0.4: version "1.0.4" @@ -3192,10 +3182,10 @@ node-environment-flags@1.0.5: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-libs-browser@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" - integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg== +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== dependencies: assert "^1.1.1" browserify-zlib "^0.2.0" @@ -3204,10 +3194,10 @@ node-libs-browser@^2.0.0: constants-browserify "^1.0.0" crypto-browserify "^3.11.0" domain-browser "^1.1.1" - events "^1.0.0" + events "^3.0.0" https-browserify "^1.0.0" os-browserify "^0.3.0" - path-browserify "0.0.0" + path-browserify "0.0.1" process "^0.11.10" punycode "^1.2.4" querystring-es3 "^0.2.0" @@ -3218,13 +3208,13 @@ node-libs-browser@^2.0.0: timers-browserify "^2.0.4" tty-browserify "0.0.0" url "^0.11.0" - util "^0.10.3" - vm-browserify "0.0.4" + util "^0.11.0" + vm-browserify "^1.0.1" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -3259,15 +3249,20 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + npm-bundled@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" - integrity sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow== + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.1.10" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" - integrity sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA== + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -3304,7 +3299,7 @@ oauth-sign@~0.8.2: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -3457,9 +3452,9 @@ p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" - integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A== + version "2.2.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" + integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== dependencies: p-try "^2.0.0" @@ -3483,34 +3478,35 @@ p-try@^1.0.0: integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" - integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== pako@~1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" - integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== parallel-transform@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" - integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= + version "1.2.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" + integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== dependencies: - cyclist "~0.2.2" + cyclist "^1.0.1" inherits "^2.0.3" readable-stream "^2.1.5" parse-asn1@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" - integrity sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw== + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" create-hash "^1.1.0" evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" parse5@4.0.0: version "4.0.0" @@ -3534,10 +3530,10 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -path-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" - integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo= +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== path-dirname@^1.0.0: version "1.0.2" @@ -3570,9 +3566,9 @@ path-to-regexp@0.1.7: integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= pbkdf2@^3.0.3: - version "3.0.16" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c" - integrity sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA== + version "3.0.17" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" + integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== dependencies: create-hash "^1.1.2" create-hmac "^1.1.4" @@ -3630,9 +3626,9 @@ prelude-ls@~1.1.2: integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== process@^0.11.10: version "0.11.10" @@ -3678,15 +3674,16 @@ psl@^1.1.24: integrity sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw== public-encrypt@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994" - integrity sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q== + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== dependencies: bn.js "^4.1.0" browserify-rsa "^4.0.0" create-hash "^1.1.0" parse-asn1 "^5.0.0" randombytes "^2.0.1" + safe-buffer "^5.1.2" pump@^2.0.0: version "2.0.1" @@ -3777,9 +3774,9 @@ querystring@0.2.0: integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" - integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -3816,7 +3813,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -3838,15 +3835,14 @@ readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readdirp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" - integrity sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg= +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== dependencies: - graceful-fs "^4.1.2" - minimatch "^3.0.2" + graceful-fs "^4.1.11" + micromatch "^3.1.10" readable-stream "^2.0.2" - set-immediate-shim "^1.0.1" reflect-metadata@^0.1.12: version "0.1.12" @@ -3867,9 +3863,9 @@ remove-trailing-separator@^1.0.1: integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" - integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo= + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== repeat-string@^1.6.1: version "1.6.1" @@ -3975,17 +3971,10 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^2.5.4, rimraf@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== - dependencies: - glob "^7.0.5" - -rimraf@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" @@ -4018,11 +4007,16 @@ rxjs@^6.1.0: dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -4054,12 +4048,17 @@ seed-random@2.2.0: resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ= -semver@^5.3.0, semver@^5.5.0: +semver@^5.3.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== -semver@^5.6.0, semver@^5.7.0: +semver@^5.7.0: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== @@ -4089,9 +4088,9 @@ send@0.17.1: statuses "~1.5.0" serialize-javascript@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" - integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== + version "1.9.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== serve-static@1.14.1: version "1.14.1" @@ -4108,25 +4107,10 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= - -set-value@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" - -set-value@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" - integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== dependencies: extend-shallow "^2.0.1" is-extendable "^0.1.1" @@ -4213,9 +4197,9 @@ snapdragon@^0.8.1: use "^3.1.0" source-list-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" - integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A== + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== source-map-loader@^0.2.4: version "0.2.4" @@ -4237,9 +4221,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@~0.5.12: - version "0.5.12" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" - integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -4318,9 +4302,9 @@ stealthy-require@^1.1.0: integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= stream-browserify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" - integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds= + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== dependencies: inherits "~2.0.1" readable-stream "^2.0.2" @@ -4375,12 +4359,12 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string_decoder@^1.0.0, string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== +string_decoder@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - safe-buffer "~5.1.0" + safe-buffer "~5.2.0" string_decoder@^1.1.1: version "1.2.0" @@ -4389,6 +4373,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.1.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -4446,49 +4437,43 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= -tapable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" - integrity sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg== - -tapable@^1.1.0: +tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tar@^4: - version "4.4.4" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd" - integrity sha512-mq9ixIYfNF9SK0IS/h2HKMu8Q2iaCuhDDsZhdEag/FHv8fOaYld4vN7ouMgcSSt5WKZzPs8atclTcJm36OTh4w== + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== dependencies: - chownr "^1.0.1" + chownr "^1.1.1" fs-minipass "^1.2.5" - minipass "^2.3.3" - minizlib "^1.1.0" + minipass "^2.3.5" + minizlib "^1.2.1" mkdirp "^0.5.0" safe-buffer "^5.1.2" - yallist "^3.0.2" + yallist "^3.0.3" -terser-webpack-plugin@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.3.0.tgz#69aa22426299f4b5b3775cbed8cb2c5d419aa1d4" - integrity sha512-W2YWmxPjjkUcOWa4pBEv4OP4er1aeQJlSo2UhtCFQCuRXEHjOFscO8VyWHj9JLlA0RzQb8Y2/Ta78XZvT54uGg== +terser-webpack-plugin@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" + integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== dependencies: - cacache "^11.3.2" - find-cache-dir "^2.0.0" + cacache "^12.0.2" + find-cache-dir "^2.1.0" is-wsl "^1.1.0" - loader-utils "^1.2.3" schema-utils "^1.0.0" serialize-javascript "^1.7.0" source-map "^0.6.1" - terser "^4.0.0" - webpack-sources "^1.3.0" + terser "^4.1.2" + webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.1.2.tgz#b2656c8a506f7ce805a3f300a2ff48db022fa391" - integrity sha512-jvNoEQSPXJdssFwqPSgWjsOrb+ELoE+ILpHPKXC83tIxOlh2U75F1KuB2luLD/3a6/7K3Vw5pDn+hvu0C4AzSw== +terser@^4.1.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.1.tgz#1052cfe17576c66e7bc70fcc7119f22b155bdac1" + integrity sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -4500,11 +4485,11 @@ text-hex@1.0.x: integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== through2@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" - integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== dependencies: - readable-stream "^2.1.5" + readable-stream "~2.3.6" xtend "~4.0.1" through@^2.3.6: @@ -4513,9 +4498,9 @@ through@^2.3.6: integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= timers-browserify@^2.0.4: - version "2.0.10" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" - integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg== + version "2.0.11" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" + integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== dependencies: setimmediate "^1.0.4" @@ -4611,11 +4596,16 @@ ts-loader@^6.0.4: micromatch "^4.0.0" semver "^6.0.0" -tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslint-consistent-codestyle@^1.13.0: version "1.13.2" resolved "https://registry.yarnpkg.com/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.13.2.tgz#c60662f08a83d62797e47e5d74737470b5e17714" @@ -4735,14 +4725,14 @@ typescript@3.5, typescript@^3.5.1: integrity sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw== union-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" - integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== dependencies: arr-union "^3.1.0" get-value "^2.0.6" is-extendable "^0.1.1" - set-value "^0.4.3" + set-value "^2.0.1" unique-filename@^1.1.1: version "1.1.1" @@ -4752,9 +4742,9 @@ unique-filename@^1.1.1: unique-slug "^2.0.0" unique-slug@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" - integrity sha1-22Z258fMBimHj/GWCXx4hVrp9Ks= + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== dependencies: imurmurhash "^0.1.4" @@ -4771,12 +4761,12 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -upath@^1.0.5: - version "1.1.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" - integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -uri-js@^4.2.1: +uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== @@ -4797,11 +4787,9 @@ url@^0.11.0: querystring "0.2.0" use@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" - integrity sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw== - dependencies: - kind-of "^6.0.2" + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== utf8@^3.0.0: version "3.0.0" @@ -4820,10 +4808,10 @@ util@0.10.3: dependencies: inherits "2.0.1" -util@^0.10.3: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== dependencies: inherits "2.0.3" @@ -4856,12 +4844,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vm-browserify@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" - integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM= - dependencies: - indexof "0.0.1" +vm-browserify@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" + integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== w3c-hr-time@^1.0.1: version "1.0.1" @@ -4870,7 +4856,7 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -watchpack@^1.5.0: +watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== @@ -4908,42 +4894,42 @@ webpack-cli@^3.1.0: v8-compile-cache "^2.0.0" yargs "^12.0.1" -webpack-sources@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" - integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== +webpack-sources@^1.4.0, webpack-sources@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: source-list-map "^2.0.0" source-map "~0.6.1" webpack@^4.35.3: - version "4.35.3" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.3.tgz#66bc35ef215a7b75e8790f84d560013ffecf0ca3" - integrity sha512-xggQPwr9ILlXzz61lHzjvgoqGU08v5+Wnut19Uv3GaTtzN4xBTcwnobodrXE142EL1tOiS5WVEButooGzcQzTA== + version "4.39.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.39.3.tgz#a02179d1032156b713b6ec2da7e0df9d037def50" + integrity sha512-BXSI9M211JyCVc3JxHWDpze85CvjC842EvpRsVTc/d15YJGlox7GIDd38kJgWrb3ZluyvIjgenbLDMBQPDcxYQ== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" "@webassemblyjs/wasm-edit" "1.8.5" "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.2.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" + acorn "^6.2.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" + eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.1" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.1" + watchpack "^1.6.0" + webpack-sources "^1.4.1" whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: version "1.0.3" @@ -5073,9 +5059,9 @@ xregexp@4.0.0: integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== xtend@^4.0.0, xtend@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== xterm-benchmark@^0.1.3: version "0.1.3" @@ -5107,10 +5093,10 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" - integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k= +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== yargs-parser@13.0.0: version "13.0.0"