diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 117b2b58..279d1b2e 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -33,6 +33,7 @@ export class AttachAddon implements ITerminalAddon { if (this._bidirectional) { this._disposables.push(terminal.onData(data => this._sendData(data))); + this._disposables.push(terminal.onBinary(data => this._sendBinary(data))); } this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); @@ -51,6 +52,17 @@ export class AttachAddon implements ITerminalAddon { } this._socket.send(data); } + + private _sendBinary(data: string): void { + if (this._socket.readyState !== 1) { + return; + } + const buffer = new Uint8Array(data.length); + for (let i = 0; i < data.length; ++i) { + buffer[i] = data.charCodeAt(i) & 255; + } + this._socket.send(buffer); + } } function addSocketListener(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable { diff --git a/src/Terminal.ts b/src/Terminal.ts index 3c589ffa..6a039a0e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -169,6 +169,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } private _onLineFeed = new EventEmitter(); @@ -221,6 +223,7 @@ 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._coreService.onBinary(e => this._onBinary.fire(e)); this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); this._instantiationService.setService(ICoreMouseService, this._coreMouseService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index e7c48381..84c0d0b7 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -32,6 +32,7 @@ export class MockTerminal implements ITerminal { onLineFeed: IEvent; onSelectionChange: IEvent; onData: IEvent; + onBinary: IEvent; onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; diff --git a/src/Types.d.ts b/src/Types.d.ts index 2a922ae1..f4d3a556 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -180,6 +180,7 @@ export interface IPublicTerminal extends IDisposable { markers: IMarker[]; onCursorMove: IEvent; onData: IEvent; + onBinary: IEvent; onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; onLineFeed: IEvent; onScroll: IEvent; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 7d9c3aa6..7e47d4b0 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -50,8 +50,10 @@ export class MockCoreService implements ICoreService { decPrivateModes: IDecPrivateModes = {} as any; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; + onBinary: IEvent = new EventEmitter().event; reset(): void {} triggerDataEvent(data: string, wasUserInput?: boolean): void {} + triggerBinaryEvent(data: string): void {} } export class MockDirtyRowService implements IDirtyRowService { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2f742038..2dcc704b 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -249,5 +249,8 @@ export interface ICoreMouseProtocol { * 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. + * Note: Returning an empty string will supress sending a mouse report, + * which can be used to skip creating falsey reports in limited encodings + * (DEFAULT only supports up to 223 1-based as coord value). */ export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index f7d3bf83..2bf011b0 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -79,6 +79,7 @@ describe('CoreMouseService', () => { cms = new CoreMouseService(bufferService, coreService); reports = []; coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); + coreService.triggerBinaryEvent = (data: string) => reports.push(data); }); it('NONE', () => { assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); @@ -143,11 +144,11 @@ describe('CoreMouseService', () => { 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]); + if (i > 222) { + // supress mouse reports if we are out of addressible range (max. 222) + assert.deepEqual(toBytes(reports.pop()), []); } else { - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]); + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); } } }); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 500655b9..0bd25dd0 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -121,15 +121,17 @@ 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). + * Can encode values up to 223 (1-based). */ 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); + const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // supress mouse report if we exceed addressible range + // Note this is handled differently by emulators + // - xterm: sends 0;0 coords instead + // - vte, konsole: no report + if (params[0] > 255 || params[1] > 255 || params[2] > 255) { + return ''; + } return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; }, /** @@ -266,7 +268,14 @@ export class CoreMouseService implements ICoreMouseService { // encode report and send const report = this._encodings[this._activeEncoding](e); - this._coreService.triggerDataEvent(report, true); + if (report) { + // always send DEFAULT as binary data + if (this._activeEncoding === 'DEFAULT') { + this._coreService.triggerBinaryEvent(report); + } else { + this._coreService.triggerDataEvent(report, true); + } + } this._lastEvent = e; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 11c3f305..35b61e84 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -23,6 +23,8 @@ export class CoreService implements ICoreService { public get onData(): IEvent { return this._onData.event; } private _onUserInput = new EventEmitter(); public get onUserInput(): IEvent { return this._onUserInput.event; } + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } constructor( // TODO: Move this into a service @@ -59,4 +61,12 @@ export class CoreService implements ICoreService { this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); this._onData.fire(data); } + + public triggerBinaryEvent(data: string): void { + if (this._optionsService.options.disableStdin) { + return; + } + this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); + this._onBinary.fire(data); + } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e4ff90d0..45d39a6a 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -68,6 +68,7 @@ export interface ICoreService { readonly onData: IEvent; readonly onUserInput: IEvent; + readonly onBinary: IEvent; reset(): void; @@ -78,8 +79,14 @@ export interface ICoreService { * resulting from parsing incoming data). When true this will also: * - Scroll to the bottom of the buffer.s * - Fire the `onUserInput` event (so selection can be cleared). - */ + */ triggerDataEvent(data: string, wasUserInput?: boolean): void; + + /** + * Triggers the onBinary event in the public API. + * @param data The data that is being emitted. + */ + triggerBinaryEvent(data: string): void; } export const IDirtyRowService = createDecorator('DirtyRowService'); diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index c167bed8..397898c7 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -27,6 +27,7 @@ export class Terminal implements ITerminalApi { public get onLineFeed(): IEvent { return this._core.onLineFeed; } public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } public get onData(): IEvent { return this._core.onData; } + public get onBinary(): IEvent { return this._core.onBinary; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } public get onScroll(): IEvent { return this._core.onScroll; } public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index f8124350..7cae660f 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -220,6 +220,7 @@ describe('Mouse Tracking Tests', () => { await page.evaluate(` window.calls = []; window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); + window.term.onBinary(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); window.term.setOption('fontSize', ${fontSize}); window.term.resize(${cols}, ${rows}); `); @@ -255,12 +256,17 @@ describe('Mouse Tracking Tests', () => { await pollFor(page, () => getReports(encoding), [{col: 51, row: 11, state: {action: 'press', 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); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // higher than 223 should not report at all + await mouseMove(257, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + await pollFor(page, () => getReports(encoding), []); // button press/move/release tests // left button @@ -511,14 +517,13 @@ describe('Mouse Tracking Tests', () => { ]); // 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); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); await pollFor(page, () => getReports(encoding), [ - {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}}} + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -821,14 +826,13 @@ describe('Mouse Tracking Tests', () => { ]); // 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); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); await pollFor(page, () => getReports(encoding), [ - {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}}} + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -1142,15 +1146,14 @@ describe('Mouse Tracking Tests', () => { ]); // 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); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); await pollFor(page, () => 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}}} + {col: 223, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 9728a72c..a086d030 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -421,6 +421,17 @@ declare module 'xterm' { */ constructor(options?: ITerminalOptions); + /** + * Adds an event listener for when a binary event fires. This is used to + * enable non UTF-8 conformant binary messages to be sent to the backend. + * Currently this is only used for a certain type of mouse reports that + * happen to be not UTF-8 compatible. + * The event value is a JS string, pass it to the underlying pty as + * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. + * @returns an `IDisposable` to stop listening. + */ + onBinary: IEvent; + /** * Adds an event listener for the cursor moves. * @returns an `IDisposable` to stop listening.