From c24eb60bd3f1ff670277ce128ed5abfb587197a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:45:19 -0800 Subject: [PATCH 01/31] Fix stack overflow in merge logic Fixes #5585 --- addons/addon-webgl/src/TextureAtlas.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 182dda59..71624fe8 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -176,6 +176,17 @@ export class TextureAtlas implements ITextureAtlas { // Gather details of the merge const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4); + + // Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot + // effectively reduce page count and merging would cause issues. + if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) { + const newPage = new AtlasPage(this._document, this._textureSize); + this._pages.push(newPage); + this._activePages.push(newPage); + this._onAddTextureAtlasCanvas.fire(newPage.canvas); + return newPage; + } + const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1); const mergedPageIndex = this.pages.length - mergingPages.length; From 48df6117ed402ca4470373b3436efad44cb1391f Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 08:31:07 -0800 Subject: [PATCH 02/31] Add ApcParser --- src/common/InputHandler.ts | 8 + src/common/parser/ApcParser.test.ts | 349 ++++++++++++++++++ src/common/parser/ApcParser.ts | 245 ++++++++++++ src/common/parser/Constants.ts | 20 +- .../parser/EscapeSequenceParser.test.ts | 52 ++- src/common/parser/EscapeSequenceParser.ts | 68 +++- src/common/parser/Types.ts | 40 +- 7 files changed, 754 insertions(+), 28 deletions(-) create mode 100644 src/common/parser/ApcParser.test.ts create mode 100644 src/common/parser/ApcParser.ts diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b2b75143..16343e88 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -19,6 +19,7 @@ import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseS import { UnicodeService } from 'common/services/UnicodeService'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; +import { ApcHandler } from 'common/parser/ApcParser'; import { IBuffer } from 'common/buffer/Types'; import { parseColor } from 'common/input/XParseColor'; import { Emitter } from 'vs/base/common/event'; @@ -712,6 +713,13 @@ export class InputHandler extends Disposable implements IInputHandler { return this._parser.registerOscHandler(ident, new OscHandler(callback)); } + /** + * Forward registerApcHandler from parser. + */ + public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._parser.registerApcHandler(ident, new ApcHandler(callback)); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/common/parser/ApcParser.test.ts b/src/common/parser/ApcParser.test.ts new file mode 100644 index 00000000..3401baab --- /dev/null +++ b/src/common/parser/ApcParser.test.ts @@ -0,0 +1,349 @@ +/** + * Copyright (c) 2025 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { ApcParser, ApcHandler } from 'common/parser/ApcParser'; +import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder'; +import { IApcHandler } 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 IApcHandler { + public id: number; + public output: [string, number, string, (boolean | string)?][]; + public msg: string; + public returnFalse: boolean; + + constructor( + id: number, + output: [string, number, string, (boolean | string)?][], + msg: string, + returnFalse: boolean = false + ) { + this.id = id; + this.output = output; + this.msg = msg; + this.returnFalse = returnFalse; + } + public start(): void { + this.output.push([this.msg, this.id, 'START']); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); + } + public end(success: boolean): boolean { + this.output.push([this.msg, this.id, 'END', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} + +describe('ApcParser', () => { + let parser: ApcParser; + let reports: [number, string, (boolean | string | undefined)?][] = []; + + beforeEach(() => { + reports = []; + parser = new ApcParser(); + parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => { + reports.push([id, action, data]); + }); + }); + + describe('identifier parsing', () => { + it('single character identifier', () => { + parser.start(); + const data = toUtf32('Gf=100,a=T;payload'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + [0x47, 'START', undefined], // 0x47 = 'G' + [0x47, 'PUT', 'f=100,a=T;payload'], + [0x47, 'END', true] + ]); + }); + + it('identifier with no payload', () => { + parser.start(); + const data = toUtf32('G'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + [0x47, 'START', undefined], + [0x47, 'END', true] + ]); + }); + + it('identifier with chunked payload', () => { + parser.start(); + let data = toUtf32('Gf=100'); + parser.put(data, 0, data.length); + data = toUtf32(',a=T'); + parser.put(data, 0, data.length); + data = toUtf32(';payload'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + [0x47, 'START', undefined], + [0x47, 'PUT', 'f=100'], + [0x47, 'PUT', ',a=T'], + [0x47, 'PUT', ';payload'], + [0x47, 'END', true] + ]); + }); + + it('empty APC sequence', () => { + parser.start(); + parser.end(true); + assert.deepEqual(reports, []); + }); + }); + + describe('handler registration', () => { + let handlerReports: [string, number, string, (boolean | string)?][]; + + beforeEach(() => { + handlerReports = []; + }); + + it('registerHandler for specific identifier', () => { + const G_CODE = 0x47; // 'G' + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty')); + parser.start(); + const data = toUtf32('Gf=100,a=T;imagedata'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(handlerReports, [ + ['kitty', G_CODE, 'START'], + ['kitty', G_CODE, 'PUT', 'f=100,a=T;imagedata'], + ['kitty', G_CODE, 'END', true] + ]); + assert.deepEqual(reports, []); + }); + + it('unregistered identifier falls back', () => { + const G_CODE = 0x47; // 'G' + const X_CODE = 0x58; // 'X' + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty')); + parser.start(); + const data = toUtf32('Xsome data'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(handlerReports, []); + assert.deepEqual(reports, [ + [X_CODE, 'START', undefined], + [X_CODE, 'PUT', 'some data'], + [X_CODE, 'END', true] + ]); + }); + + it('clearHandler removes handler', () => { + const G_CODE = 0x47; + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty')); + parser.clearHandler(G_CODE); + parser.start(); + const data = toUtf32('Gf=100'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(handlerReports, []); + assert.deepEqual(reports, [ + [G_CODE, 'START', undefined], + [G_CODE, 'PUT', 'f=100'], + [G_CODE, 'END', true] + ]); + }); + + it('multiple handlers for same identifier', () => { + const G_CODE = 0x47; + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1')); + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2')); + parser.start(); + const data = toUtf32('Gdata'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(handlerReports, [ + ['handler2', G_CODE, 'START'], + ['handler1', G_CODE, 'START'], + ['handler2', G_CODE, 'PUT', 'data'], + ['handler1', G_CODE, 'PUT', 'data'], + ['handler2', G_CODE, 'END', true], + ['handler1', G_CODE, 'END', false] + ]); + }); + + it('handler returning false allows fallthrough', () => { + const G_CODE = 0x47; + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1')); + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2', true)); + parser.start(); + const data = toUtf32('Gdata'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(handlerReports, [ + ['handler2', G_CODE, 'START'], + ['handler1', G_CODE, 'START'], + ['handler2', G_CODE, 'PUT', 'data'], + ['handler1', G_CODE, 'PUT', 'data'], + ['handler2', G_CODE, 'END', true], + ['handler1', G_CODE, 'END', true] + ]); + }); + + it('dispose removes handler', () => { + const G_CODE = 0x47; + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1')); + const disposable = parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2')); + disposable.dispose(); + parser.start(); + const data = toUtf32('Gdata'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(handlerReports, [ + ['handler1', G_CODE, 'START'], + ['handler1', G_CODE, 'PUT', 'data'], + ['handler1', G_CODE, 'END', true] + ]); + }); + }); + + describe('ApcHandler convenience class', () => { + it('should be called once on end(true)', () => { + const G_CODE = 0x47; + const results: [number, string][] = []; + parser.registerHandler(G_CODE, new ApcHandler((data: string) => { + results.push([G_CODE, data]); + return true; + })); + parser.start(); + let data = toUtf32('Gf=100'); + parser.put(data, 0, data.length); + data = toUtf32(',a=T;payload'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(results, [[G_CODE, 'f=100,a=T;payload']]); + }); + + it('should not be called on end(false)', () => { + const G_CODE = 0x47; + const results: [number, string][] = []; + parser.registerHandler(G_CODE, new ApcHandler((data: string) => { + results.push([G_CODE, data]); + return true; + })); + parser.start(); + const data = toUtf32('Gf=100,a=T;payload'); + parser.put(data, 0, data.length); + parser.end(false); + assert.deepEqual(results, []); + }); + + it('should handle payload up to limit', function(): void { + this.timeout(30000); + const G_CODE = 0x47; + const results: [number, string][] = []; + parser.registerHandler(G_CODE, new ApcHandler((data: string) => { + results.push([G_CODE, data]); + return true; + })); + parser.start(); + let data = toUtf32('G'); + 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(results, [[G_CODE, 'A'.repeat(PAYLOAD_LIMIT)]]); + }); + + it('should abort for payload over limit', function(): void { + this.timeout(30000); + const G_CODE = 0x47; + const results: [number, string][] = []; + parser.registerHandler(G_CODE, new ApcHandler((data: string) => { + results.push([G_CODE, data]); + return true; + })); + parser.start(); + let data = toUtf32('G'); + 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(results, []); + }); + }); + + describe('reset behavior', () => { + let handlerReports: [string, number, string, (boolean | string)?][]; + + beforeEach(() => { + handlerReports = []; + }); + + it('reset during payload cleans up handlers', () => { + const G_CODE = 0x47; + parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty')); + parser.start(); + const data = toUtf32('Gf=100'); + parser.put(data, 0, data.length); + parser.reset(); + assert.deepEqual(handlerReports, [ + ['kitty', G_CODE, 'START'], + ['kitty', G_CODE, 'PUT', 'f=100'], + ['kitty', G_CODE, 'END', false] + ]); + }); + }); +}); + +describe('ApcParser - async tests', () => { + let parser: ApcParser; + let reports: [number, string, (boolean | string | undefined)?][] = []; + + beforeEach(() => { + reports = []; + parser = new ApcParser(); + parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => { + reports.push([id, action, data]); + }); + }); + + async function endP(parser: ApcParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.end(success, prev)) { + prev = await result; + } + } + + describe('async ApcHandler', () => { + it('should handle async handler', async () => { + const G_CODE = 0x47; + const results: [number, string][] = []; + parser.registerHandler(G_CODE, new ApcHandler(async (data: string) => { + await new Promise(res => setTimeout(res, 10)); + results.push([G_CODE, data]); + return true; + })); + parser.start(); + const data = toUtf32('Gf=100,a=T'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(results, [[G_CODE, 'f=100,a=T']]); + }); + }); +}); diff --git a/src/common/parser/ApcParser.ts b/src/common/parser/ApcParser.ts new file mode 100644 index 00000000..81f26860 --- /dev/null +++ b/src/common/parser/ApcParser.ts @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2025 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from 'common/parser/Types'; +import { ApcState, PAYLOAD_LIMIT } from 'common/parser/Constants'; +import { utf32ToString } from 'common/input/TextDecoder'; +import { IDisposable } from 'common/Types'; + +const EMPTY_HANDLERS: IApcHandler[] = []; + +/** + * APC Parser for handling Application Program Command sequences. + * APC sequences use the format: ESC _ ESC \ + * + * Unlike OSC which uses numeric identifiers (e.g., OSC 1337), + * APC uses the first character as the identifier (e.g., 'G' for Kitty graphics). + * The identifier is the character code of the first byte after ESC _. + */ +export class ApcParser implements IApcParser { + private _state = ApcState.START; + private _active = EMPTY_HANDLERS; + private _id = -1; + private _handlers: IHandlerCollection = Object.create(null); + private _handlerFb: ApcFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + + /** + * Register an APC handler for a specific identifier. + * @param ident The character code of the first byte (e.g., 0x47 for 'G') + * @param handler The handler to register + */ + public registerHandler(ident: number, handler: IApcHandler): 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 clearHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; + } + + public setHandlerFallback(handler: ApcFallbackHandlerType): void { + this._handlerFb = handler; + } + + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => { }; + this._active = EMPTY_HANDLERS; + } + + public reset(): void { + // force cleanup handlers if payload was already sent + if (this._state === ApcState.PAYLOAD) { + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].end(false); + } + } + this._stack.paused = false; + this._active = EMPTY_HANDLERS; + this._id = -1; + this._state = ApcState.START; + } + + private _start(): void { + this._active = this._handlers[this._id] || EMPTY_HANDLERS; + if (!this._active.length) { + this._handlerFb(this._id, 'START'); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].start(); + } + } + } + + private _put(data: Uint32Array, start: number, end: number): void { + if (!this._active.length) { + this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); + } + } + } + + public start(): void { + // always reset leftover handlers + this.reset(); + this._state = ApcState.ID; + } + + /** + * Put data to current APC command. + * For APC, the first character is used as the identifier. + * Format: ESC _ ESC \ + * Example: ESC _ G f=100,a=T;... ESC \ (Kitty graphics, identifier='G') + */ + public put(data: Uint32Array, start: number, end: number): void { + if (this._state === ApcState.ABORT) { + return; + } + if (this._state === ApcState.ID) { + // The first character is the identifier + if (start < end) { + this._id = data[start++]; + this._state = ApcState.PAYLOAD; + this._start(); + } + } + if (this._state === ApcState.PAYLOAD && end - start > 0) { + this._put(data, start, end); + } + } + + /** + * Indicates end of an APC command. + * Whether the APC got aborted or finished normally + * is indicated by `success`. + */ + public end(success: boolean, promiseResult: boolean = true): void | Promise { + if (this._state === ApcState.START) { + return; + } + // do nothing if command was faulty + if (this._state !== ApcState.ABORT) { + // if we are still in ID state and get an early end + // means we got an empty APC sequence with no identifier, + // which is invalid - just reset and return + if (this._state === ApcState.ID) { + this._active = EMPTY_HANDLERS; + this._id = -1; + this._state = ApcState.START; + return; + } + + if (!this._active.length) { + this._handlerFb(this._id, 'END', success); + } else { + let handlerResult: boolean | Promise = false; + let j = this._active.length - 1; + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; + } + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + handlerResult = this._active[j].end(success); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + } + j--; + } + // cleanup left over handlers + // we always have to call .end for proper cleanup, + // here we use `success` to indicate whether a handler should execute + for (; j >= 0; j--) { + handlerResult = this._active[j].end(false); + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } + } + } + + } + this._active = EMPTY_HANDLERS; + this._id = -1; + this._state = ApcState.START; + } +} + +/** + * Convenient class to allow attaching string based handler functions + * as APC handlers. + */ +export class ApcHandler implements IApcHandler { + private _data = ''; + private _hitLimit: boolean = false; + + constructor(private _handler: (data: string) => boolean | Promise) { } + + 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): boolean | Promise { + let ret: boolean | Promise = false; + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._data); + if (ret instanceof Promise) { + // need to hold data until `ret` got resolved + // dont care for errors, data will be freed anyway on next start + return ret.then(res => { + this._data = ''; + this._hitLimit = false; + return res; + }); + } + } + this._data = ''; + this._hitLimit = false; + return ret; + } +} diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index 7fe24f34..f9c9c347 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -20,7 +20,8 @@ export const enum ParserState { DCS_PARAM = 10, DCS_IGNORE = 11, DCS_INTERMEDIATE = 12, - DCS_PASSTHROUGH = 13 + DCS_PASSTHROUGH = 13, + APC_STRING = 14 } /** @@ -41,7 +42,10 @@ export const enum ParserAction { CLEAR = 11, DCS_HOOK = 12, DCS_PUT = 13, - DCS_UNHOOK = 14 + DCS_UNHOOK = 14, + APC_START = 15, + APC_PUT = 16, + APC_END = 17 } /** @@ -54,5 +58,17 @@ export const enum OscState { ABORT = 3 } +// I actually don't know if below is correct.. +// If it is.. it seems like dup of OscState. +/** + * Internal states of ApcParser. + */ +export const enum ApcState { + 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/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 76e88dea..5c754a20 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -705,17 +705,17 @@ describe('EscapeSequenceParser', () => { }); it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', () => { parser.reset(); - // C0 - let initializers = ['\x58', '\x5e', '\x5f']; + // C0 (only SOS and PM, APC has separate handling) + let initializers = ['\x58', '\x5e']; for (let i = 0; i < initializers.length; ++i) { parse(parser, '\x1b' + initializers[i]); assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); parser.reset(); } - // C1 + // C1 (only SOS and PM, APC has separate handling) for (state in states) { parser.currentState = state; - initializers = ['\x98', '\x9e', '\x9f']; + initializers = ['\x98', '\x9e']; for (let i = 0; i < initializers.length; ++i) { parse(parser, initializers[i]); assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); @@ -723,6 +723,20 @@ describe('EscapeSequenceParser', () => { } } }); + it('trans ANYWHERE/ESCAPE --> APC_STRING', () => { + parser.reset(); + // C0 (ESC _) + parse(parser, '\x1b_'); + assert.equal(parser.currentState, ParserState.APC_STRING); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parse(parser, '\x9f'); + assert.equal(parser.currentState, ParserState.APC_STRING); + parser.reset(); + } + }); it('state SOS_PM_APC_STRING ignore rules', () => { parser.reset(); let ignored = r(0x00, 0x18); @@ -1207,7 +1221,7 @@ describe('EscapeSequenceParser', () => { clearAccu(); }); it('print handler', () => { - parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { + parser2.setPrintHandler(function(data: Uint32Array, start: number, end: number): void { for (let i = start; i < end; ++i) { print += stringFromCodePoint(data[i]); } @@ -1221,11 +1235,11 @@ describe('EscapeSequenceParser', () => { assert.equal(print, ''); }); it('ESC handler', () => { - parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean { + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function(): boolean { esc.push('%G'); return true; }); - parser2.registerEscHandler({ final: 'E' }, function (): boolean { + parser2.registerEscHandler({ final: 'E' }, function(): boolean { esc.push('E'); return true; }); @@ -1293,7 +1307,7 @@ describe('EscapeSequenceParser', () => { }); }); it('CSI handler', () => { - parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean { + parser2.registerCsiHandler({ final: 'm' }, function(params: IParams): boolean { csi.push(['m', params.toArray(), '']); return true; }); @@ -1373,11 +1387,11 @@ describe('EscapeSequenceParser', () => { }); }); it('EXECUTE handler', () => { - parser2.setExecuteHandler('\n', function (): boolean { + parser2.setExecuteHandler('\n', function(): boolean { exe.push('\n'); return true; }); - parser2.setExecuteHandler('\r', function (): boolean { + parser2.setExecuteHandler('\r', function(): boolean { exe.push('\r'); return true; }); @@ -1390,7 +1404,7 @@ describe('EscapeSequenceParser', () => { assert.deepEqual(exe, ['\n']); }); it('OSC handler', () => { - parser2.registerOscHandler(1, new OscHandler(function (data: string): boolean { + parser2.registerOscHandler(1, new OscHandler(function(data: string): boolean { osc.push([1, data]); return true; })); @@ -1471,17 +1485,17 @@ describe('EscapeSequenceParser', () => { }); it('DCS handler', () => { parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, { - hook: function (params: IParams): void { + hook: function(params: IParams): void { dcs.push(['hook', '', params.toArray(), 0]); }, - put: function (data: Uint32Array, start: number, end: number): void { + put: function(data: Uint32Array, start: number, end: number): void { let s = ''; for (let i = start; i < end; ++i) { s += stringFromCodePoint(data[i]); } dcs.push(['put', s]); }, - unhook: function (): boolean { + unhook: function(): boolean { dcs.push(['unhook']); return true; } @@ -1560,7 +1574,7 @@ describe('EscapeSequenceParser', () => { }); it('ERROR handler', () => { let errorState: IParsingState | null = null; - parser2.setErrorHandler(function (state: IParsingState): IParsingState { + parser2.setErrorHandler(function(state: IParsingState): IParsingState { errorState = state; return state; }); @@ -1788,7 +1802,7 @@ describe('EscapeSequenceParser - async', () => { parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; })); - parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); + parser.registerDcsHandler({ final: 'a' }, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true; })); }); it('sync handlers keep being parsed in sync mode', () => { @@ -1823,7 +1837,7 @@ describe('EscapeSequenceParser - async', () => { parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; })); - parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); + parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true; })); }); it('sync parse call does not work anymore', () => { @@ -2156,7 +2170,7 @@ describe('EscapeSequenceParser - async', () => { }); it('multiple async DCS handlers', async () => { // register with fallback - const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;})); + const DCS2 = parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false; })); await parseP(parser, INPUT); for (let i = 0; i < callstack.length; ++i) { const entry = callstack[i]; @@ -2187,7 +2201,7 @@ describe('EscapeSequenceParser - async', () => { clearAccu(); // register without fallback - const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;})); + const DCS22 = parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true; })); await parseP(parser, INPUT); for (let i = 0; i < callstack.length; ++i) { const entry = callstack[i]; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index e13d67f3..710ac10f 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'common/Types'; import { Params } from 'common/parser/Params'; import { OscParser } from 'common/parser/OscParser'; import { DcsParser } from 'common/parser/DcsParser'; +import { ApcParser } from 'common/parser/ApcParser'; /** * Table values are generated like this: @@ -75,7 +76,7 @@ const NON_ASCII_PRINTABLE = 0xA0; * VT500 compatible transition table. * Taken from https://vt100.net/emu/dec_ansi_parser. */ -export const VT500_TRANSITION_TABLE = (function (): TransitionTable { +export const VT500_TRANSITION_TABLE = (function(): TransitionTable { const table: TransitionTable = new TransitionTable(4095); // range macro for byte @@ -104,7 +105,8 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC - table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // SOS, PM + table.add(0x9f, state, ParserAction.APC_START, ParserState.APC_STRING); // APC table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS } @@ -128,8 +130,9 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND); table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); - // sos/pm/apc does nothing - table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + // sos/pm/apc - sos/pm does nothing, apc gets separate handling + table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // SOS, PM + table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING); // APC ('_') table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); @@ -187,12 +190,18 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { 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, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + // apc passthrough - similar to DCS passthrough + table.addMany(PRINTABLES, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING); + table.addMany(EXECUTABLES, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING); + table.add(0x7f, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING); + table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_STRING, ParserAction.APC_END, 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); table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.add(NON_ASCII_PRINTABLE, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING); return table; })(); @@ -243,6 +252,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _escHandlers: IHandlerCollection; protected readonly _oscParser: IOscParser; protected readonly _dcsParser: IDcsParser; + protected readonly _apcParser: IApcParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers @@ -290,6 +300,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP })); this._oscParser = this._register(new OscParser()); this._dcsParser = this._register(new DcsParser()); + this._apcParser = this._register(new ApcParser()); this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) @@ -425,6 +436,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._oscParser.setHandlerFallback(handler); } + public registerApcHandler(ident: number, handler: IApcHandler): IDisposable { + return this._apcParser.registerHandler(ident, handler); + } + public clearApcHandler(ident: number): void { + this._apcParser.clearHandler(ident); + } + public setApcHandlerFallback(handler: ApcFallbackHandlerType): void { + this._apcParser.setHandlerFallback(handler); + } + public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { this._errorHandler = callback; } @@ -445,6 +466,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.currentState = this.initialState; this._oscParser.reset(); this._dcsParser.reset(); + this._apcParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; @@ -606,6 +628,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.addParam(0); // ZDM this._collect = 0; break; + case ParserStackType.APC: + code = data[this._parseStack.chunkPos]; + handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; } // cleanup before continuing with the main sync loop this._parseStack.state = ParserStackType.NONE; @@ -785,6 +818,31 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._collect = 0; this.precedingJoinState = 0; break; + case ParserAction.APC_START: + this._apcParser.start(); + break; + case ParserAction.APC_PUT: + // inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c + for (let j = i + 1; ; ++j) { + if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || code === 0x9c || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { + this._apcParser.put(data, i, j); + i = j - 1; + break; + } + } + break; + case ParserAction.APC_END: + handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a); + if (handlerResult) { + this._preserveStack(ParserStackType.APC, [], 0, transition, i); + return handlerResult; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + this.precedingJoinState = 0; + break; } this.currentState = transition & TableAccess.TRANSITION_STATE_MASK; } diff --git a/src/common/parser/Types.ts b/src/common/parser/Types.ts index 2ed4acdc..474d8160 100644 --- a/src/common/parser/Types.ts +++ b/src/common/parser/Types.ts @@ -36,7 +36,7 @@ export interface IParams { addSubParam(value: number): void; hasSubParams(idx: number): boolean; getSubParams(idx: number): Int32Array | null; - getSubParamsAll(): {[idx: number]: Int32Array}; + getSubParamsAll(): { [idx: number]: Int32Array }; } /** @@ -134,6 +134,32 @@ export interface IOscHandler { } export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; +/** + * APC handler types. + * APC (Application Program Command) sequences are used by protocols like + * Kitty Graphics Protocol. Format: ESC _ ESC \ + */ +export interface IApcHandler { + /** + * Announces start of this APC 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 APC 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): boolean | Promise; +} +export type ApcFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; + /** * PRINT handler types. */ @@ -196,6 +222,10 @@ export interface IEscapeSequenceParser extends IDisposable { clearOscHandler(ident: number): void; setOscHandlerFallback(handler: OscFallbackHandlerType): void; + registerApcHandler(ident: number, handler: IApcHandler): IDisposable; + clearApcHandler(ident: number): void; + setApcHandlerFallback(handler: ApcFallbackHandlerType): void; + setErrorHandler(handler: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; } @@ -223,6 +253,11 @@ export interface IDcsParser extends ISubParser; } +export interface IApcParser extends ISubParser { + start(): void; + end(success: boolean, promiseResult?: boolean): void | Promise; +} + /** * Interface to denote a specific ESC, CSI or DCS handler slot. * The values are used to create an integer respresentation during handler @@ -252,7 +287,8 @@ export const enum ParserStackType { CSI, ESC, OSC, - DCS + DCS, + APC } // aggregate of resumable handler lists From 8e29591b9f72c980dfe21f2a527b1627cf201393 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 10:42:36 -0800 Subject: [PATCH 03/31] Add registerApcHandler to public api --- src/browser/TestUtils.test.ts | 3 +++ src/common/CoreTerminal.ts | 5 +++++ src/common/Types.ts | 2 ++ src/common/public/ParserApi.ts | 3 +++ typings/xterm.d.ts | 18 ++++++++++++++++++ 5 files changed, 31 insertions(+) diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 5777d147..44a9bc77 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -107,6 +107,9 @@ export class MockTerminal implements ITerminal { public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } + public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + throw new Error('Method not implemented.'); + } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { throw new Error('Method not implemented.'); } diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index dd312af1..01dded5e 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -242,6 +242,11 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { return this._inputHandler.registerOscHandler(ident, callback); } + /** Add handler for APC escape sequence. See xterm.d.ts for details. */ + public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._inputHandler.registerApcHandler(ident, callback); + } + protected _setup(): void { this._handleWindowsPtyOptionChange(); } diff --git a/src/common/Types.ts b/src/common/Types.ts index 269b30c8..25d39d5f 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -22,6 +22,7 @@ export interface ICoreTerminal { registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; + registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; } export interface IDisposable { @@ -475,6 +476,7 @@ export interface IInputHandler { registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; + registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; /** C0 BEL */ bell(): boolean; /** C0 LF */ lineFeed(): boolean; diff --git a/src/common/public/ParserApi.ts b/src/common/public/ParserApi.ts index afcc01be..a9b97188 100644 --- a/src/common/public/ParserApi.ts +++ b/src/common/public/ParserApi.ts @@ -34,4 +34,7 @@ export class ParserApi implements IParser { public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { return this.registerOscHandler(ident, callback); } + public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._core.registerApcHandler(ident, callback); + } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ee3364dd..604e9504 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1932,6 +1932,24 @@ declare module '@xterm/xterm' { * @returns An IDisposable you can call to remove this handler. */ registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; + + /** + * Adds a handler for APC escape sequences. + * @param ident The identifier (first character) of the sequence as a + * character code, e.g. 71 for 'G' (Kitty graphics protocol). + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished successfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since APC sequences are + * not limited by the amount of data this might impose a problem for big + * payloads. Currently xterm.js limits APC payload to 10 MB which should + * give enough room for most use cases. The callback is called with APC data + * string (excluding the identifier character). Return `true` if the + * sequence was handled, `false` if the parser should try a previous + * handler. The most recently added handler is tried first. + * @returns An IDisposable you can call to remove this handler. + */ + registerApcHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; } /** From 1b2d361b59afdc035c9d9dada105c6cadc8fd415 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 10:59:33 -0800 Subject: [PATCH 04/31] Remove comment on Constant.ts for ApcState --- src/common/parser/Constants.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index f9c9c347..1468c3bb 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -58,8 +58,6 @@ export const enum OscState { ABORT = 3 } -// I actually don't know if below is correct.. -// If it is.. it seems like dup of OscState. /** * Internal states of ApcParser. */ From 63ebe2df5bb7209292e7084b56b8b2efc61b6ae7 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 11:00:10 -0800 Subject: [PATCH 05/31] Stop messing with existing formatting --- src/common/parser/Types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/parser/Types.ts b/src/common/parser/Types.ts index 474d8160..04eabbea 100644 --- a/src/common/parser/Types.ts +++ b/src/common/parser/Types.ts @@ -36,7 +36,7 @@ export interface IParams { addSubParam(value: number): void; hasSubParams(idx: number): boolean; getSubParams(idx: number): Int32Array | null; - getSubParamsAll(): { [idx: number]: Int32Array }; + getSubParamsAll(): {[idx: number]: Int32Array}; } /** From 5ecd980a83660d0939f1a486c1a11502f25c2e36 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 11:01:09 -0800 Subject: [PATCH 06/31] Comment --- src/common/parser/Types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/common/parser/Types.ts b/src/common/parser/Types.ts index 04eabbea..2405dcb8 100644 --- a/src/common/parser/Types.ts +++ b/src/common/parser/Types.ts @@ -136,8 +136,6 @@ export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | ' /** * APC handler types. - * APC (Application Program Command) sequences are used by protocols like - * Kitty Graphics Protocol. Format: ESC _ ESC \ */ export interface IApcHandler { /** @@ -147,7 +145,6 @@ export interface IApcHandler { start(): void; /** * Incoming data chunk. - * Note: Data is borrowed. */ put(data: Uint32Array, start: number, end: number): void; /** From 42d408a9a76eea9ad7a2b9d07acfd65a8687a74b Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 11:29:04 -0800 Subject: [PATCH 07/31] Change ordering in escapeSequnceParser.ts --- src/common/parser/Constants.ts | 2 +- .../parser/EscapeSequenceParser.test.ts | 14 +++++----- src/common/parser/EscapeSequenceParser.ts | 26 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index 1468c3bb..fec3cee3 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -14,7 +14,7 @@ export const enum ParserState { CSI_PARAM = 4, CSI_INTERMEDIATE = 5, CSI_IGNORE = 6, - SOS_PM_APC_STRING = 7, + SOS_PM_STRING = 7, OSC_STRING = 8, DCS_ENTRY = 9, DCS_PARAM = 10, diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 5c754a20..799b43d2 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -156,7 +156,7 @@ const states: number[] = [ ParserState.CSI_PARAM, ParserState.CSI_INTERMEDIATE, ParserState.CSI_IGNORE, - ParserState.SOS_PM_APC_STRING, + ParserState.SOS_PM_STRING, ParserState.OSC_STRING, ParserState.DCS_ENTRY, ParserState.DCS_PARAM, @@ -703,13 +703,13 @@ describe('EscapeSequenceParser', () => { testTerminal.clear(); } }); - it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', () => { + it('trans ANYWHERE/ESCAPE --> SOS_PM_STRING', () => { parser.reset(); // C0 (only SOS and PM, APC has separate handling) let initializers = ['\x58', '\x5e']; for (let i = 0; i < initializers.length; ++i) { parse(parser, '\x1b' + initializers[i]); - assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); + assert.equal(parser.currentState, ParserState.SOS_PM_STRING); parser.reset(); } // C1 (only SOS and PM, APC has separate handling) @@ -718,7 +718,7 @@ describe('EscapeSequenceParser', () => { initializers = ['\x98', '\x9e']; for (let i = 0; i < initializers.length; ++i) { parse(parser, initializers[i]); - assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); + assert.equal(parser.currentState, ParserState.SOS_PM_STRING); parser.reset(); } } @@ -737,16 +737,16 @@ describe('EscapeSequenceParser', () => { parser.reset(); } }); - it('state SOS_PM_APC_STRING ignore rules', () => { + it('state SOS_PM_STRING ignore rules', () => { parser.reset(); let ignored = r(0x00, 0x18); ignored = ignored.concat(['\x19']); ignored = ignored.concat(r(0x1c, 0x20)); ignored = ignored.concat(r(0x20, 0x80)); for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.SOS_PM_APC_STRING; + parser.currentState = ParserState.SOS_PM_STRING; parse(parser, ignored[i]); - assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); + assert.equal(parser.currentState, ParserState.SOS_PM_STRING); parser.reset(); } }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 710ac10f..60396c1e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -105,7 +105,7 @@ export const VT500_TRANSITION_TABLE = (function(): TransitionTable { table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC - table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // SOS, PM + table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM table.add(0x9f, state, ParserAction.APC_START, ParserState.APC_STRING); // APC table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS @@ -130,13 +130,18 @@ export const VT500_TRANSITION_TABLE = (function(): TransitionTable { table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND); table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); - // sos/pm/apc - sos/pm does nothing, apc gets separate handling - table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // SOS, PM - table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING); // APC ('_') - table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); - table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); - table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); - table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + // sos/pm + table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING); + table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING); + table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING); + table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING); + // apc + table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING); + table.addMany(PRINTABLES, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING); + table.addMany(EXECUTABLES, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING); + table.add(0x7f, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING); + table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_STRING, ParserAction.APC_END, ParserState.GROUND); // csi entries table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); @@ -190,11 +195,6 @@ export const VT500_TRANSITION_TABLE = (function(): TransitionTable { 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, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); - // apc passthrough - similar to DCS passthrough - table.addMany(PRINTABLES, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING); - table.addMany(EXECUTABLES, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING); - table.add(0x7f, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING); - table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_STRING, ParserAction.APC_END, 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); From 03b015d5d3298251c57578290cf1e0f0d54f69ff Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 11:45:21 -0800 Subject: [PATCH 08/31] Add registerApcHandler to xterm-headless.d.ts --- typings/xterm-headless.d.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index c12d6ba9..35bfac07 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -1312,6 +1312,24 @@ declare module '@xterm/headless' { * @returns An IDisposable you can call to remove this handler. */ registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + + /** + * Adds a handler for APC escape sequences. + * @param ident The identifier (first character) of the sequence as a + * character code, e.g. 71 for 'G' (Kitty graphics protocol). + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished successfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since APC sequences are + * not limited by the amount of data this might impose a problem for big + * payloads. Currently xterm.js limits APC payload to 10 MB which should + * give enough room for most use cases. The callback is called with APC data + * string (excluding the identifier character). Return true if the sequence + * was handled; false if we should try a previous handler. The most recently + * added handler is tried first. + * @returns An IDisposable you can call to remove this handler. + */ + registerApcHandler(ident: number, callback: (data: string) => boolean): IDisposable; } /** From 1c73006e39ee70332f8c48fa521bddf5a33df64d Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 13:41:29 -0800 Subject: [PATCH 09/31] Switch TransitionTable to Uint16Array --- src/common/parser/EscapeSequenceParser.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 60396c1e..2eb36bbb 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -18,8 +18,8 @@ import { ApcParser } from 'common/parser/ApcParser'; * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState */ const enum TableAccess { - TRANSITION_ACTION_SHIFT = 4, - TRANSITION_STATE_MASK = 15, + TRANSITION_ACTION_SHIFT = 8, + TRANSITION_STATE_MASK = 255, INDEX_STATE_SHIFT = 8 } @@ -27,10 +27,10 @@ const enum TableAccess { * Transition table for EscapeSequenceParser. */ export class TransitionTable { - public table: Uint8Array; + public table: Uint16Array; constructor(length: number) { - this.table = new Uint8Array(length); + this.table = new Uint16Array(length); } /** From b01ba7db28ce9fd10d103d2bbcf6f9fa3db5d85c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 14:17:01 -0800 Subject: [PATCH 10/31] Add apc tests parser.test.ts --- test/playwright/Parser.test.ts | 107 +++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/test/playwright/Parser.test.ts b/test/playwright/Parser.test.ts index b793b00e..989d7fd3 100644 --- a/test/playwright/Parser.test.ts +++ b/test/playwright/Parser.test.ts @@ -21,6 +21,7 @@ declare global { customDcsHandlerCallStack?: [string, (number | number[])[], string][]; customEscHandlerCallStack?: string[]; customOscHandlerCallStack?: string[][]; + customApcHandlerCallStack?: string[][]; disposable?: IDisposable; disposables?: IDisposable[]; } @@ -241,4 +242,110 @@ test.describe('Parser Integration Tests', () => { ]); }); }); + test.describe('registerApcHandler', () => { + // TODO: Remove when Kitty Graphics tests added (real-world usage replaces this) + test('should call custom APC handler with identifier', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customApcHandlerCallStack = []; + // APC uses first character as identifier (e.g., 0x41 = 'A') + window.disposable = term.parser.registerApcHandler(0x41, data => { + window.customApcHandlerCallStack!.push(['handler', data]); + return true; + }); + }); + // APC format: ESC _ ESC \ + await ctx.proxy.write('\x1b_Asome data here\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [ + ['handler', 'some data here'] + ]); + }); + // TODO: Remove when Kitty Graphics tests added + test('should handle short data', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customApcHandlerCallStack = []; + window.disposable = term.parser.registerApcHandler(0x42, data => { + window.customApcHandlerCallStack!.push(['handler', data]); + return true; + }); + }); + // Short APC with minimal data + await ctx.proxy.write('\x1b_Bhi\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [ + ['handler', 'hi'] + ]); + }); + test('should respect return value', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customApcHandlerCallStack = []; + window.disposables = [ + term.parser.registerApcHandler(0x43, data => { + window.customApcHandlerCallStack!.push(['A', data]); + return false; + }), + term.parser.registerApcHandler(0x43, data => { + window.customApcHandlerCallStack!.push(['B', data]); + return true; + }), + term.parser.registerApcHandler(0x43, data => { + window.customApcHandlerCallStack!.push(['C', data]); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b_Csome data\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [ + ['C', 'some data'], + ['B', 'some data'] + ]); + }); + test('async', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customApcHandlerCallStack = []; + window.disposables = [ + term.parser.registerApcHandler(0x44, data => { + window.customApcHandlerCallStack!.push(['A', data]); + return false; + }), + term.parser.registerApcHandler(0x44, data => { + return new Promise(res => setTimeout(res, 50)).then(() => { + window.customApcHandlerCallStack!.push(['B', data]); + return false; + }); + }), + term.parser.registerApcHandler(0x44, data => { + window.customApcHandlerCallStack!.push(['C', data]); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b_Dsome data\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [ + ['C', 'some data'], + ['B', 'some data'], + ['A', 'some data'] + ]); + }); + // TODO: Remove when Kitty Graphics tests added + test('should handle different identifiers independently', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customApcHandlerCallStack = []; + window.disposables = [ + term.parser.registerApcHandler(0x46, data => { // 'F' + window.customApcHandlerCallStack!.push(['F', data]); + return true; + }), + term.parser.registerApcHandler(0x58, data => { // 'X' + window.customApcHandlerCallStack!.push(['X', data]); + return true; + }) + ]; + }); + await ctx.proxy.write('\x1b_Ffirst data\x1b\\'); + await ctx.proxy.write('\x1b_Xsecond data\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [ + ['F', 'first data'], + ['X', 'second data'] + ]); + }); + }); }); From 3d92a7e2edc502fa236a3d029219c38ecda00c40 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 15:13:03 -0800 Subject: [PATCH 11/31] Dont be silly with original formatting --- .../parser/EscapeSequenceParser.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 799b43d2..9cc5bc88 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -1221,7 +1221,7 @@ describe('EscapeSequenceParser', () => { clearAccu(); }); it('print handler', () => { - parser2.setPrintHandler(function(data: Uint32Array, start: number, end: number): void { + parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { for (let i = start; i < end; ++i) { print += stringFromCodePoint(data[i]); } @@ -1235,11 +1235,11 @@ describe('EscapeSequenceParser', () => { assert.equal(print, ''); }); it('ESC handler', () => { - parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function(): boolean { + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean { esc.push('%G'); return true; }); - parser2.registerEscHandler({ final: 'E' }, function(): boolean { + parser2.registerEscHandler({ final: 'E' }, function (): boolean { esc.push('E'); return true; }); @@ -1307,7 +1307,7 @@ describe('EscapeSequenceParser', () => { }); }); it('CSI handler', () => { - parser2.registerCsiHandler({ final: 'm' }, function(params: IParams): boolean { + parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean { csi.push(['m', params.toArray(), '']); return true; }); @@ -1387,11 +1387,11 @@ describe('EscapeSequenceParser', () => { }); }); it('EXECUTE handler', () => { - parser2.setExecuteHandler('\n', function(): boolean { + parser2.setExecuteHandler('\n', function (): boolean { exe.push('\n'); return true; }); - parser2.setExecuteHandler('\r', function(): boolean { + parser2.setExecuteHandler('\r', function (): boolean { exe.push('\r'); return true; }); @@ -1404,7 +1404,7 @@ describe('EscapeSequenceParser', () => { assert.deepEqual(exe, ['\n']); }); it('OSC handler', () => { - parser2.registerOscHandler(1, new OscHandler(function(data: string): boolean { + parser2.registerOscHandler(1, new OscHandler(function (data: string): boolean { osc.push([1, data]); return true; })); @@ -1485,17 +1485,17 @@ describe('EscapeSequenceParser', () => { }); it('DCS handler', () => { parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, { - hook: function(params: IParams): void { + hook: function (params: IParams): void { dcs.push(['hook', '', params.toArray(), 0]); }, - put: function(data: Uint32Array, start: number, end: number): void { + put: function (data: Uint32Array, start: number, end: number): void { let s = ''; for (let i = start; i < end; ++i) { s += stringFromCodePoint(data[i]); } dcs.push(['put', s]); }, - unhook: function(): boolean { + unhook: function (): boolean { dcs.push(['unhook']); return true; } @@ -1574,7 +1574,7 @@ describe('EscapeSequenceParser', () => { }); it('ERROR handler', () => { let errorState: IParsingState | null = null; - parser2.setErrorHandler(function(state: IParsingState): IParsingState { + parser2.setErrorHandler(function (state: IParsingState): IParsingState { errorState = state; return state; }); From b286f21f649d432d8942751d242f9125556c3de6 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 15:50:51 -0800 Subject: [PATCH 12/31] Add APC_STRING since DCS_PASSTHROUGH isnt the last in ParserState anymore --- src/common/parser/EscapeSequenceParser.test.ts | 6 ++++-- src/common/parser/EscapeSequenceParser.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 9cc5bc88..08d47f06 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -162,7 +162,8 @@ const states: number[] = [ ParserState.DCS_PARAM, ParserState.DCS_IGNORE, ParserState.DCS_INTERMEDIATE, - ParserState.DCS_PASSTHROUGH + ParserState.DCS_PASSTHROUGH, + ParserState.APC_STRING ]; let state: any; @@ -276,7 +277,8 @@ describe('EscapeSequenceParser', () => { ]; const exceptions: { [key: number]: { [key: string]: any[] } } = { 8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING - 13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] } // abort DCS_PASSTHROUGH + 13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] }, // abort DCS_PASSTHROUGH + 14: { '\x18': [], '\x1a': [] } // abort APC_STRING }; parser.reset(); testTerminal.clear(); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 2eb36bbb..3757ac93 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -90,7 +90,7 @@ export const VT500_TRANSITION_TABLE = (function(): TransitionTable { EXECUTABLES.push(0x19); EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); - const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1); + const states: number[] = r(ParserState.GROUND, ParserState.APC_STRING + 1); let state: any; // set default transition From ee3b3626e8c1a3a8b30e1a52b6bf684cd2114e94 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Jan 2026 16:27:19 -0800 Subject: [PATCH 13/31] Again, format --- src/common/parser/EscapeSequenceParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 3757ac93..ef627c9d 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -76,7 +76,7 @@ const NON_ASCII_PRINTABLE = 0xA0; * VT500 compatible transition table. * Taken from https://vt100.net/emu/dec_ansi_parser. */ -export const VT500_TRANSITION_TABLE = (function(): TransitionTable { +export const VT500_TRANSITION_TABLE = (function (): TransitionTable { const table: TransitionTable = new TransitionTable(4095); // range macro for byte From 2c1441a52d8a1b5a8fe418195892848b43a36308 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Jan 2026 10:49:21 -0800 Subject: [PATCH 14/31] Add STATE_LENGTH to equal last state + 1 --- src/common/parser/Constants.ts | 4 +++- src/common/parser/EscapeSequenceParser.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index fec3cee3..b066654c 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -21,7 +21,9 @@ export const enum ParserState { DCS_IGNORE = 11, DCS_INTERMEDIATE = 12, DCS_PASSTHROUGH = 13, - APC_STRING = 14 + APC_STRING = 14, + // Number of states. States must be continuous and this must be LAST_STATE + 1. + STATE_LENGTH = 15 } /** diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index ef627c9d..d11579e8 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -90,7 +90,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { EXECUTABLES.push(0x19); EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); - const states: number[] = r(ParserState.GROUND, ParserState.APC_STRING + 1); + const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH); let state: any; // set default transition From 1ace1084222514096d789590bbd814a5f2d2075b Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Jan 2026 10:54:41 -0800 Subject: [PATCH 15/31] better comments --- src/common/parser/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index b066654c..547e5407 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -22,7 +22,7 @@ export const enum ParserState { DCS_INTERMEDIATE = 12, DCS_PASSTHROUGH = 13, APC_STRING = 14, - // Number of states. States must be continuous and this must be LAST_STATE + 1. + // Number of states, meaning LAST_STATE + 1. STATE_LENGTH = 15 } From 62dc0d9c5196732e012b1702b02ceb747f59a9fe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:12:37 -0800 Subject: [PATCH 16/31] Don't set windows input mode on demo --- demo/client/client.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/demo/client/client.ts b/demo/client/client.ts index 1ed83669..68202ad7 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -282,10 +282,7 @@ function createTerminal(): Terminal { buildNumber: 22621 } : undefined, fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"', - theme: { ...xtermjsTheme }, - vtExtensions: { - win32InputMode: isWindows - } + theme: { ...xtermjsTheme } } as ITerminalOptions); // Load addons From 2af9d10171514f7986b85a2576950d1c9e59bc66 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:44:08 -0800 Subject: [PATCH 17/31] Don't disambiguous kitty keyboard on shift only Part of microsoft/vscode#286809 --- src/common/input/KittyKeyboard.test.ts | 15 +++++++++------ src/common/input/KittyKeyboard.ts | 9 +++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts index 3e068f46..e346708f 100644 --- a/src/common/input/KittyKeyboard.test.ts +++ b/src/common/input/KittyKeyboard.test.ts @@ -33,12 +33,14 @@ describe('KittyKeyboard', () => { describe('modifier encoding (value = 1 + modifiers)', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; - it('shift=2 (1+1)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true }), flags); - assert.strictEqual(result.key, '\x1b[97;2u'); + it('shift+letter sends plain character in DISAMBIGUATE mode', () => { + // Kitty spec: DISAMBIGUATE only encodes keys ambiguous in legacy encoding + // Shift+a → "A" is not ambiguous, so send plain "A" + const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags); + assert.strictEqual(result.key, 'A'); }); - it('alt=3 (1+2)', () => { + it('alt=3 (1+2) still uses CSI u', () => { const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', altKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;3u'); }); @@ -598,9 +600,10 @@ describe('KittyKeyboard', () => { describe('edge cases', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; - it('always uses lowercase codepoint for letters', () => { + it('shift+letter sends plain character in DISAMBIGUATE mode', () => { + // Shift+A produces printable "A", not ambiguous, so send plain character const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags); - assert.strictEqual(result.key, '\x1b[97;2u'); + assert.strictEqual(result.key, 'A'); }); it('ctrl+shift+a sends lowercase codepoint 97', () => { diff --git a/src/common/input/KittyKeyboard.ts b/src/common/input/KittyKeyboard.ts index 0dfd0b56..f2d7388b 100644 --- a/src/common/input/KittyKeyboard.ts +++ b/src/common/input/KittyKeyboard.ts @@ -351,8 +351,13 @@ export function evaluateKeyboardEventKitty( } else if (isFunc) { useCsiU = true; } else if (modifiers > 0) { - // Any modified key - useCsiU = true; + // Shift-only + printable character (e.g., Shift+a → "A") should NOT use CSI u + // per Kitty spec: DISAMBIGUATE only encodes keys ambiguous in legacy encoding + if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) { + useCsiU = false; + } else { + useCsiU = true; + } } } From b87cba4c2afcd3685b63ad1caccbf59d2709a85e Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 28 Jan 2026 14:04:46 -0800 Subject: [PATCH 18/31] Update node-pty to ^1.2.0-beta.9 --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c218b70..f20b41a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "jsdom": "^27.3.0", "mocha": "^10.1.0", "mustache": "^4.2.0", - "node-pty": "1.1.0-beta19", + "node-pty": "^1.2.0-beta.9", "nyc": "^17.1.0", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", @@ -6016,11 +6016,12 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta19", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta19.tgz", - "integrity": "sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==", + "version": "1.2.0-beta.9", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.9.tgz", + "integrity": "sha512-XR3x/4OQZ7DlceespOQ99kk9jZHwmO78RuEIZtGsSPdsDi11s79VcK04aqcaK3x09Y6ORJM/KBz/oQ/p+WGRLg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "node-addon-api": "^7.1.0" } diff --git a/package.json b/package.json index 9b54946c..c319f0b3 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "jsdom": "^27.3.0", "mocha": "^10.1.0", "mustache": "^4.2.0", - "node-pty": "1.1.0-beta19", + "node-pty": "^1.2.0-beta.9", "nyc": "^17.1.0", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", From e8d99d50a64dea97b71c864b4663faa160be1f4e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:03:16 -0800 Subject: [PATCH 19/31] Remove vs dependency in common Fixes #5631 --- addons/addon-webgl/src/DevicePixelObserver.ts | 2 +- addons/addon-webgl/src/GlyphRenderer.ts | 2 +- addons/addon-webgl/src/RectangleRenderer.ts | 2 +- addons/addon-webgl/src/TextureAtlas.ts | 2 +- addons/addon-webgl/src/Types.ts | 2 +- addons/addon-webgl/src/WebglAddon.ts | 4 +- addons/addon-webgl/src/WebglRenderer.ts | 4 +- .../src/renderLayer/BaseRenderLayer.ts | 2 +- headless/package.json | 14 +-- src/browser/AccessibilityManager.ts | 2 +- src/browser/CoreBrowserTerminal.ts | 4 +- src/browser/Linkifier.ts | 4 +- src/browser/TestUtils.test.ts | 2 +- src/browser/Types.ts | 2 +- src/browser/Viewport.ts | 4 +- .../decorations/BufferDecorationRenderer.ts | 2 +- .../decorations/OverviewRulerRenderer.ts | 2 +- src/browser/public/Terminal.ts | 4 +- src/browser/renderer/dom/DomRenderer.ts | 4 +- src/browser/renderer/shared/Types.ts | 2 +- src/browser/services/CharSizeService.ts | 4 +- src/browser/services/CoreBrowserService.ts | 4 +- src/browser/services/LinkProviderService.ts | 2 +- src/browser/services/RenderService.ts | 4 +- src/browser/services/SelectionService.ts | 4 +- src/browser/services/Services.ts | 2 +- src/browser/services/ThemeService.ts | 4 +- src/common/CircularList.ts | 4 +- src/common/CoreTerminal.ts | 4 +- src/common/Event.ts | 105 ++++++++++++++++++ src/common/InputHandler.ts | 4 +- src/common/Lifecycle.ts | 100 +++++++++++++++++ src/common/TestUtils.test.ts | 2 +- src/common/Types.ts | 2 +- src/common/buffer/BufferSet.ts | 4 +- src/common/buffer/Marker.ts | 4 +- src/common/buffer/Types.ts | 2 +- src/common/input/WriteBuffer.ts | 4 +- src/common/parser/EscapeSequenceParser.ts | 2 +- src/common/public/BufferNamespaceApi.ts | 4 +- src/common/services/BufferService.ts | 4 +- src/common/services/CoreMouseService.ts | 4 +- src/common/services/CoreService.ts | 4 +- src/common/services/DecorationService.test.ts | 4 +- src/common/services/DecorationService.ts | 4 +- src/common/services/LogService.ts | 2 +- src/common/services/OptionsService.ts | 4 +- src/common/services/Services.ts | 2 +- src/common/services/UnicodeService.ts | 2 +- src/common/tsconfig.json | 5 +- src/headless/Terminal.ts | 2 +- src/headless/public/Terminal.ts | 4 +- 52 files changed, 286 insertions(+), 86 deletions(-) create mode 100644 src/common/Event.ts create mode 100644 src/common/Lifecycle.ts diff --git a/addons/addon-webgl/src/DevicePixelObserver.ts b/addons/addon-webgl/src/DevicePixelObserver.ts index 33d733ef..dcd8de0a 100644 --- a/addons/addon-webgl/src/DevicePixelObserver.ts +++ b/addons/addon-webgl/src/DevicePixelObserver.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable, IDisposable } from 'common/Lifecycle'; export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { // Observe any resizes to the element and extract the actual pixel size of the element if the diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 6b359d4b..f5be446f 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -5,7 +5,7 @@ import { TextureAtlas } from './TextureAtlas'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { Terminal } from '@xterm/xterm'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types'; import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils'; diff --git a/addons/addon-webgl/src/RectangleRenderer.ts b/addons/addon-webgl/src/RectangleRenderer.ts index 8ace9076..d22068ba 100644 --- a/addons/addon-webgl/src/RectangleRenderer.ts +++ b/addons/addon-webgl/src/RectangleRenderer.ts @@ -7,7 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { IThemeService } from 'browser/services/Services'; import { ReadonlyColorSet } from 'browser/Types'; import { Attributes, FgFlags } from 'common/buffer/Constants'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IColor } from 'common/Types'; import { Terminal } from '@xterm/xterm'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 182dda59..2c6ed2ad 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -15,7 +15,7 @@ import { IColor } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; import { IUnicodeService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; /** * A shared object which is used to draw nothing for a particular cell. diff --git a/addons/addon-webgl/src/Types.ts b/addons/addon-webgl/src/Types.ts index e030e579..3e734a0d 100644 --- a/addons/addon-webgl/src/Types.ts +++ b/addons/addon-webgl/src/Types.ts @@ -7,7 +7,7 @@ import { FontWeight } from '@xterm/xterm'; import { IColorSet } from 'browser/Types'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types'; -import type { Event } from 'vs/base/common/event'; +import type { Event } from 'common/Event'; export interface IRenderModel { cells: Uint32Array; diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 72e0835e..0bc24c90 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -7,13 +7,13 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm'; import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { ITerminal } from 'browser/Types'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { getSafariVersion, isSafari } from 'common/Platform'; import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; import { IWebGL2RenderingContext } from './Types'; import { WebglRenderer } from './WebglRenderer'; import { setTraceLogger } from 'common/services/LogService'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'common/Event'; export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi { private _terminal?: Terminal; diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 345437d5..1bdde448 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -22,9 +22,9 @@ import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET import { IWebGL2RenderingContext, type ITextureAtlas } from './Types'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { IRenderLayer } from './renderLayer/Types'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; -import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; export class WebglRenderer extends Disposable implements IRenderer { diff --git a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts index 85b170e7..5b116153 100644 --- a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -7,7 +7,7 @@ import { ReadonlyColorSet } from 'browser/Types'; import { acquireTextureAtlas } from '../CharAtlasCache'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; import { Terminal } from '@xterm/xterm'; diff --git a/headless/package.json b/headless/package.json index b8e71974..fda5eae6 100644 --- a/headless/package.json +++ b/headless/package.json @@ -1,17 +1,15 @@ { "name": "@xterm/headless", "description": "A headless terminal component that runs in Node.js", - "version": "5.5.0", + "version": "6.0.0", "main": "lib-headless/xterm-headless.js", - "module": "lib-headless/xterm-headless.mjs", + "module": "lib/xterm.mjs", "types": "typings/xterm-headless.d.ts", - "exports": { - "types": "./typings/xterm-headless.d.ts", - "import": "./lib-headless/xterm-headless.mjs", - "require": "./lib-headless/xterm-headless.js" - }, "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", + "workspaces": [ + "addons/*" + ], "keywords": [ "cli", "command-line", @@ -27,4 +25,4 @@ "webgl", "xterm" ] -} +} \ No newline at end of file diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index f5784230..e202b819 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -6,7 +6,7 @@ import * as Strings from 'browser/LocalizableStrings'; import { ITerminal, IRenderDebouncer } from 'browser/Types'; import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IInstantiationService } from 'common/services/Services'; diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index c26e6705..f743bd00 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -55,9 +55,9 @@ import { IDecorationService } from 'common/services/Services'; import { WindowsOptionsReportType } from '../common/InputHandler'; import { AccessibilityManager } from './AccessibilityManager'; import { Linkifier } from './Linkifier'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; -import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { MutableDisposable, toDisposable } from 'common/Lifecycle'; export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { public textarea: HTMLTextAreaElement | undefined; diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index c5c31d62..d579b7fe 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -4,11 +4,11 @@ */ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types'; -import { Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { IBufferService } from 'common/services/Services'; import { ILinkProviderService, IMouseService, IRenderService } from './services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; export class Linkifier extends Disposable implements ILinkifier2 { diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 5777d147..2d83ae53 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -18,7 +18,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { css } from 'common/Color'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; -import { Emitter, type Event } from 'vs/base/common/event'; +import { Emitter, type Event } from 'common/Event'; export class TestTerminal extends CoreBrowserTerminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } diff --git a/src/browser/Types.ts b/src/browser/Types.ts index 77f6a61c..5c840bff 100644 --- a/src/browser/Types.ts +++ b/src/browser/Types.ts @@ -7,7 +7,7 @@ import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types' import { IBuffer } from 'common/buffer/Types'; import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm'; import { channels, css } from 'common/Color'; -import type { Event } from 'vs/base/common/event'; +import type { Event } from 'common/Event'; /** * A portion of the public API that are implemented identially internally and simply passed through. diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index a550f8b4..33442976 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -5,13 +5,13 @@ import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { ViewportConstants } from 'browser/shared/Constants'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services'; import { CoreMouseEventType } from 'common/Types'; import { addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'common/Event'; import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable'; import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch'; diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index 7b9a3fde..e374af26 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services'; export class BufferDecorationRenderer extends Disposable { diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index c8efb2a5..ea36cf76 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -5,7 +5,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; const enum Constants { diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 442f8b6a..16006905 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -6,14 +6,14 @@ import * as Strings from 'browser/LocalizableStrings'; import { CoreBrowserTerminal as TerminalCore } from 'browser/CoreBrowserTerminal'; import { IBufferRange, ITerminal } from 'browser/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { ITerminalOptions } from 'common/Types'; import { AddonManager } from 'common/public/AddonManager'; import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; import { ParserApi } from 'common/public/ParserApi'; import { UnicodeApi } from 'common/public/UnicodeApi'; import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm'; -import type { Event } from 'vs/base/common/event'; +import type { Event } from 'common/Event'; /** * The set of options that only have an effect when set in the Terminal constructor. diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 4349b222..c564ca4a 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -12,9 +12,9 @@ import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderMode import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types'; import { color } from 'common/Color'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; diff --git a/src/browser/renderer/shared/Types.ts b/src/browser/renderer/shared/Types.ts index de913e9b..05f462a7 100644 --- a/src/browser/renderer/shared/Types.ts +++ b/src/browser/renderer/shared/Types.ts @@ -6,7 +6,7 @@ import { Terminal } from '@xterm/xterm'; import { ITerminal } from 'browser/Types'; import { IDisposable } from 'common/Types'; -import type { Event } from 'vs/base/common/event'; +import type { Event } from 'common/Event'; export interface IDimensions { width: number; diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 5d231055..6baf9b50 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -5,8 +5,8 @@ import { IOptionsService } from 'common/services/Services'; import { ICharSizeService } from 'browser/services/Services'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; export class CharSizeService extends Disposable implements ICharSizeService { public serviceBrand: undefined; diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts index d8ea0a64..562793f2 100644 --- a/src/browser/services/CoreBrowserService.ts +++ b/src/browser/services/CoreBrowserService.ts @@ -4,9 +4,9 @@ */ import { ICoreBrowserService } from './Services'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; -import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; export class CoreBrowserService extends Disposable implements ICoreBrowserService { public serviceBrand: undefined; diff --git a/src/browser/services/LinkProviderService.ts b/src/browser/services/LinkProviderService.ts index a8687e59..7a7b530d 100644 --- a/src/browser/services/LinkProviderService.ts +++ b/src/browser/services/LinkProviderService.ts @@ -1,5 +1,5 @@ import { ILinkProvider, ILinkProviderService } from 'browser/services/Services'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; export class LinkProviderService extends Disposable implements ILinkProviderService { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index f2a60512..1cced406 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -7,10 +7,10 @@ import { RenderDebouncer } from 'browser/RenderDebouncer'; import { IRenderDebouncerWithCallback } from 'browser/Types'; import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; -import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { DebouncedIdleTask } from 'common/TaskQueue'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; interface ISelectionState { start: [number, number] | undefined; diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 6abd5fb9..95fa88cd 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -9,14 +9,14 @@ import { moveToCellSequence } from 'browser/input/MoveToCell'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import * as Browser from 'common/Platform'; import { IBufferLine, IDisposable } from 'common/Types'; import { getRangeLength } from 'common/buffer/BufferRange'; import { CellData } from 'common/buffer/CellData'; import { IBuffer } from 'common/buffer/Types'; import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; /** * The number of pixels the mouse needs to be above or below the viewport in diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 535afa64..af9211df 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -8,7 +8,7 @@ import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { AllColorIndex, IDisposable, IKeyboardResult } from 'common/Types'; -import type { Event } from 'vs/base/common/event'; +import type { Event } from 'common/Event'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts index cd85e0ff..f2b23f3c 100644 --- a/src/browser/services/ThemeService.ts +++ b/src/browser/services/ThemeService.ts @@ -7,10 +7,10 @@ import { ColorContrastCache } from 'browser/ColorContrastCache'; import { IThemeService } from 'browser/services/Services'; import { DEFAULT_ANSI_COLORS, IColorContrastCache, IColorSet, ReadonlyColorSet } from 'browser/Types'; import { color, css, NULL_COLOR } from 'common/Color'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { IOptionsService, ITheme } from 'common/services/Services'; import { AllColorIndex, IColor, SpecialColorIndex } from 'common/Types'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; interface IRestoreColorSet { foreground: IColor; diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index a46faca3..3663bfc1 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -4,8 +4,8 @@ */ import { ICircularList } from 'common/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; export interface IInsertEvent { index: number; diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index dd312af1..cff8558e 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -37,8 +37,8 @@ import { IBufferSet } from 'common/buffer/Types'; import { InputHandler } from 'common/InputHandler'; import { WriteBuffer } from 'common/input/WriteBuffer'; import { OscLinkService } from 'common/services/OscLinkService'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'common/Event'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; // Only trigger this warning a single time per session let hasWriteSyncWarnHappened = false; diff --git a/src/common/Event.ts b/src/common/Event.ts new file mode 100644 index 00000000..2239725c --- /dev/null +++ b/src/common/Event.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved. + * @license MIT + * + * Minimal event utilities for xterm.js core. + * Simplified from VS Code's event.ts - no leak detection/profiling. + */ + +import { IDisposable, DisposableStore, toDisposable } from 'common/Lifecycle'; + +export interface Event { + (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable; +} + +export class Emitter { + private _listeners: Array<{ fn: (e: T) => any; thisArgs: any }> = []; + private _disposed = false; + private _event: Event | undefined; + + public get event(): Event { + if (!this._event) { + this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { + if (this._disposed) { + return toDisposable(() => {}); + } + + const entry = { fn: listener, thisArgs }; + this._listeners.push(entry); + + const result = toDisposable(() => { + const idx = this._listeners.indexOf(entry); + if (idx !== -1) { + this._listeners.splice(idx, 1); + } + }); + + if (disposables) { + if (Array.isArray(disposables)) { + disposables.push(result); + } else { + disposables.add(result); + } + } + + return result; + }; + } + return this._event; + } + + public fire(event: T): void { + if (this._disposed) { + return; + } + // Snapshot listeners to allow modifications during iteration + const listeners = this._listeners.slice(); + for (const { fn, thisArgs } of listeners) { + fn.call(thisArgs, event); + } + } + + public dispose(): void { + if (this._disposed) { + return; + } + this._disposed = true; + this._listeners.length = 0; + } +} + +export namespace Event { + export function forward(from: Event, to: Emitter): IDisposable { + return from(e => to.fire(e)); + } + + export function map(event: Event, map: (i: I) => O): Event { + return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { + return event(i => listener.call(thisArgs, map(i)), undefined, disposables); + }; + } + + export function any(...events: Event[]): Event; + export function any(...events: Event[]): Event; + export function any(...events: Event[]): Event { + return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { + const store = new DisposableStore(); + for (const event of events) { + store.add(event(e => listener.call(thisArgs, e))); + } + if (disposables) { + if (Array.isArray(disposables)) { + disposables.push(store); + } else { + disposables.add(store); + } + } + return store; + }; + } + + export function runAndSubscribe(event: Event, handler: (e: T | undefined) => void): IDisposable { + handler(undefined); + return event(e => handler(e)); + } +} diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b2b75143..71e0a1c6 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -8,7 +8,7 @@ import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; @@ -21,7 +21,7 @@ import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; import { IBuffer } from 'common/buffer/Types'; import { parseColor } from 'common/input/XParseColor'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; import { XTERM_VERSION } from 'common/Version'; /** diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts new file mode 100644 index 00000000..8d7001cd --- /dev/null +++ b/src/common/Lifecycle.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved. + * @license MIT + * + * Minimal lifecycle utilities for xterm.js core. + * Simplified from VS Code's lifecycle.ts - no tracking/leak detection. + */ + +export interface IDisposable { + dispose(): void; +} + +export function toDisposable(fn: () => void): IDisposable { + return { dispose: fn }; +} + +export function dispose(disposables: T | T[] | undefined): void { + if (!disposables) { + return; + } + if (Array.isArray(disposables)) { + for (const d of disposables) { + d.dispose(); + } + } else { + disposables.dispose(); + } +} + +export function combinedDisposable(...disposables: IDisposable[]): IDisposable { + return toDisposable(() => dispose(disposables)); +} + +export class DisposableStore implements IDisposable { + private readonly _disposables = new Set(); + private _isDisposed = false; + + public get isDisposed(): boolean { + return this._isDisposed; + } + + public add(o: T): T { + if (this._isDisposed) { + o.dispose(); + } else { + this._disposables.add(o); + } + return o; + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + for (const d of this._disposables) { + d.dispose(); + } + this._disposables.clear(); + } +} + +export abstract class Disposable implements IDisposable { + protected readonly _store = new DisposableStore(); + + public dispose(): void { + this._store.dispose(); + } + + protected _register(o: T): T { + return this._store.add(o); + } +} + +export class MutableDisposable implements IDisposable { + private _value: T | undefined; + private _isDisposed = false; + + public get value(): T | undefined { + return this._isDisposed ? undefined : this._value; + } + + public set value(value: T | undefined) { + if (this._isDisposed || value === this._value) { + return; + } + this._value?.dispose(); + this._value = value; + } + + public clear(): void { + this.value = undefined; + } + + public dispose(): void { + this._isDisposed = true; + this._value?.dispose(); + this._value = undefined; + } +} diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 2d44c32e..cbe4fe58 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -12,7 +12,7 @@ import { BufferSet } from 'common/buffer/BufferSet'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData, IDisposable } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; import { IDecorationOptions, IDecoration } from '@xterm/xterm'; -import { Emitter, type Event } from 'vs/base/common/event'; +import { Emitter, type Event } from 'common/Event'; export class MockBufferService implements IBufferService { public serviceBrand: any; diff --git a/src/common/Types.ts b/src/common/Types.ts index 269b30c8..aeeee5b2 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -9,7 +9,7 @@ import { IBufferSet } from 'common/buffer/Types'; import { IParams } from 'common/parser/Types'; import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from '@xterm/xterm'; -import type { Emitter, Event } from 'vs/base/common/event'; +import type { Emitter, Event } from 'common/Event'; export interface ICoreTerminal { coreMouseService: ICoreMouseService; diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index 8f3a6aec..d83d3925 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { IAttributeData } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferService, IOptionsService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; /** * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index a83cf876..59ad049b 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -4,8 +4,8 @@ */ import { IDisposable, IMarker } from 'common/Types'; -import { Emitter } from 'vs/base/common/event'; -import { dispose } from 'vs/base/common/lifecycle'; +import { Emitter } from 'common/Event'; +import { dispose } from 'common/Lifecycle'; export class Marker implements IMarker { private static _nextId = 1; diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts index c6e7a53f..b6e83586 100644 --- a/src/common/buffer/Types.ts +++ b/src/common/buffer/Types.ts @@ -4,7 +4,7 @@ */ import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker, ICharset, IDisposable } from 'common/Types'; -import type { Event } from 'vs/base/common/event'; +import type { Event } from 'common/Event'; // BufferIndex denotes a position in the buffer: [rowIndex, colIndex] export type BufferIndex = [number, number]; diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index f6d83ba8..c9db076d 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -4,8 +4,8 @@ * @license MIT */ -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; declare const setTimeout: (handler: () => void, timeout?: number) => void; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index e13d67f3..77fd1283 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -5,7 +5,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { Params } from 'common/parser/Params'; import { OscParser } from 'common/parser/OscParser'; diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts index e8508608..425b1798 100644 --- a/src/common/public/BufferNamespaceApi.ts +++ b/src/common/public/BufferNamespaceApi.ts @@ -6,8 +6,8 @@ import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from '@xterm/xterm'; import { BufferApiView } from 'common/public/BufferApiView'; import { ICoreTerminal } from 'common/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; export class BufferNamespaceApi extends Disposable implements IBufferNamespaceApi { private _normal: BufferApiView; diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 4cba3c15..2e6227fe 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { IAttributeData, IBufferLine } from 'common/Types'; import { BufferSet } from 'common/buffer/BufferSet'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferService, IOptionsService, type IBufferResizeEvent } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index a10ddafb..d569f046 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -4,8 +4,8 @@ */ import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; /** * Supported default protocols. diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 9981ba21..f2a15d8e 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -4,10 +4,10 @@ */ import { clone } from 'common/Clone'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { IDecPrivateModes, IKittyKeyboardState, IModes } from 'common/Types'; import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; const DEFAULT_MODES: IModes = Object.freeze({ insertMode: false diff --git a/src/common/services/DecorationService.test.ts b/src/common/services/DecorationService.test.ts index d7e459e1..365be087 100644 --- a/src/common/services/DecorationService.test.ts +++ b/src/common/services/DecorationService.test.ts @@ -6,8 +6,8 @@ import { assert } from 'chai'; import { DecorationService } from './DecorationService'; import { IMarker } from 'common/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; function createFakeMarker(line: number): IMarker { return Object.freeze(new class extends Disposable { diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index e3cafa8f..92e06089 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -4,12 +4,12 @@ */ import { css } from 'common/Color'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; import { SortedList } from 'common/SortedList'; import { IColor } from 'common/Types'; import { IDecoration, IDecorationOptions, IMarker } from '@xterm/xterm'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; // Work variables to avoid garbage collection let $xmin = 0; diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index 986ff628..12c5e3b0 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'common/Lifecycle'; import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services'; type LogType = (message?: any, ...optionalParams: any[]) => void; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 4ae8bf96..2eab930f 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { isMac } from 'common/Platform'; import { CursorStyle, IDisposable } from 'common/Types'; import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; export const DEFAULT_OPTIONS: Readonly> = { cols: 80, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index bbf36771..737dcd0d 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -7,7 +7,7 @@ import { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, ty import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from 'common/Types'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; -import type { Emitter, Event } from 'vs/base/common/event'; +import type { Emitter, Event } from 'common/Event'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { diff --git a/src/common/services/UnicodeService.ts b/src/common/services/UnicodeService.ts index 12e4ec41..d0dd8584 100644 --- a/src/common/services/UnicodeService.ts +++ b/src/common/services/UnicodeService.ts @@ -5,7 +5,7 @@ import { UnicodeV6 } from 'common/input/UnicodeV6'; import { IUnicodeService, IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'common/Event'; export class UnicodeService implements IUnicodeService { public serviceBrand: any; diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index e9b3673a..28f5205c 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -11,14 +11,11 @@ ], "baseUrl": "..", "paths": { - "vs/*": [ "./vs/*" ] + "common/*": [ "./common/*" ] } }, "include": [ "./**/*", "../../typings/xterm.d.ts" - ], - "references": [ - { "path": "../vs" } ] } diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 1425699e..52618e86 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -25,7 +25,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBuffer } from 'common/buffer/Types'; import { CoreTerminal } from 'common/CoreTerminal'; import { IMarker, ITerminalOptions } from 'common/Types'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'common/Event'; export class Terminal extends CoreTerminal { private readonly _onBell = this._register(new Emitter()); diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 18659b3c..17b143fa 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -10,8 +10,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITer import { Terminal as TerminalCore } from 'headless/Terminal'; import { AddonManager } from 'common/public/AddonManager'; import { ITerminalOptions } from 'common/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; -import type { Event } from 'vs/base/common/event'; +import { Disposable } from 'common/Lifecycle'; +import type { Event } from 'common/Event'; /** * The set of options that only have an effect when set in the Terminal constructor. */ From b58bd452afc88012471e62ff215dff5c046f5d2d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:07:30 -0800 Subject: [PATCH 20/31] Fix lint --- src/common/Event.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/common/Event.ts b/src/common/Event.ts index 2239725c..44146e76 100644 --- a/src/common/Event.ts +++ b/src/common/Event.ts @@ -8,16 +8,16 @@ import { IDisposable, DisposableStore, toDisposable } from 'common/Lifecycle'; -export interface Event { +export interface IEvent { (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable; } export class Emitter { - private _listeners: Array<{ fn: (e: T) => any; thisArgs: any }> = []; + private _listeners: { fn: (e: T) => any, thisArgs: any }[] = []; private _disposed = false; - private _event: Event | undefined; + private _event: IEvent | undefined; - public get event(): Event { + public get event(): IEvent { if (!this._event) { this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { if (this._disposed) { @@ -69,19 +69,19 @@ export class Emitter { } export namespace Event { - export function forward(from: Event, to: Emitter): IDisposable { + export function forward(from: IEvent, to: Emitter): IDisposable { return from(e => to.fire(e)); } - export function map(event: Event, map: (i: I) => O): Event { + export function map(event: IEvent, map: (i: I) => O): IEvent { return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { return event(i => listener.call(thisArgs, map(i)), undefined, disposables); }; } - export function any(...events: Event[]): Event; - export function any(...events: Event[]): Event; - export function any(...events: Event[]): Event { + export function any(...events: IEvent[]): IEvent; + export function any(...events: IEvent[]): IEvent; + export function any(...events: IEvent[]): IEvent { return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { const store = new DisposableStore(); for (const event of events) { @@ -98,7 +98,7 @@ export namespace Event { }; } - export function runAndSubscribe(event: Event, handler: (e: T | undefined) => void): IDisposable { + export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable { handler(undefined); return event(e => handler(e)); } From f80dbf3d03d1076b42c5a02c82253b93defdd29d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:21:15 -0800 Subject: [PATCH 21/31] Fix lint, split up namespace and interface --- addons/addon-webgl/src/Types.ts | 6 +-- addons/addon-webgl/src/WebglAddon.ts | 10 ++-- addons/addon-webgl/src/WebglRenderer.ts | 6 +-- src/browser/CoreBrowserTerminal.ts | 22 ++++----- src/browser/TestUtils.test.ts | 54 +++++++++++----------- src/browser/Types.ts | 20 ++++---- src/browser/Viewport.ts | 6 +-- src/browser/public/Terminal.ts | 28 +++++------ src/browser/renderer/shared/Types.ts | 4 +- src/browser/services/CoreBrowserService.ts | 4 +- src/browser/services/Services.ts | 26 +++++------ src/common/CoreTerminal.ts | 14 +++--- src/common/Event.ts | 2 +- src/common/TestUtils.test.ts | 20 ++++---- src/common/Types.ts | 12 ++--- src/common/buffer/Types.ts | 4 +- src/common/services/Services.ts | 24 +++++----- src/headless/Terminal.ts | 12 ++--- src/headless/public/Terminal.ts | 22 ++++----- 19 files changed, 148 insertions(+), 148 deletions(-) diff --git a/addons/addon-webgl/src/Types.ts b/addons/addon-webgl/src/Types.ts index 3e734a0d..67c9408d 100644 --- a/addons/addon-webgl/src/Types.ts +++ b/addons/addon-webgl/src/Types.ts @@ -7,7 +7,7 @@ import { FontWeight } from '@xterm/xterm'; import { IColorSet } from 'browser/Types'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; export interface IRenderModel { cells: Uint32Array; @@ -58,8 +58,8 @@ export interface ICharAtlasConfig { export interface ITextureAtlas extends IDisposable { readonly pages: { canvas: HTMLCanvasElement, version: number }[]; - onAddTextureAtlasCanvas: Event; - onRemoveTextureAtlasCanvas: Event; + onAddTextureAtlasCanvas: IEvent; + onRemoveTextureAtlasCanvas: IEvent; /** * Warm up the texture atlas, adding common glyphs to avoid slowing early frame. diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 0bc24c90..7dc3ed35 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -13,7 +13,7 @@ import { ICoreService, IDecorationService, ILogService, IOptionsService } from ' import { IWebGL2RenderingContext } from './Types'; import { WebglRenderer } from './WebglRenderer'; import { setTraceLogger } from 'common/services/LogService'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils } from 'common/Event'; export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi { private _terminal?: Terminal; @@ -85,10 +85,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi this._customGlyphs, this._preserveDrawingBuffer )); - this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss)); - this._register(Event.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); - this._register(Event.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); - this._register(Event.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)); + this._register(EventUtils.forward(this._renderer.onContextLoss, this._onContextLoss)); + this._register(EventUtils.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + this._register(EventUtils.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); + this._register(EventUtils.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)); renderService.setRenderer(this._renderer); this._register(toDisposable(() => { diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 1bdde448..05af6527 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -22,7 +22,7 @@ import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET import { IWebGL2RenderingContext, type ITextureAtlas } from './Types'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { IRenderLayer } from './renderLayer/Types'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; @@ -290,8 +290,8 @@ export class WebglRenderer extends Disposable implements IRenderer { if (this._charAtlas !== atlas) { this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); this._charAtlasDisposable.value = combinedDisposable( - Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas), - Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas) + EventUtils.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas), + EventUtils.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas) ); } this._charAtlas = atlas; diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index f743bd00..869a545a 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -55,7 +55,7 @@ import { IDecorationService } from 'common/services/Services'; import { WindowsOptionsReportType } from '../common/InputHandler'; import { AccessibilityManager } from './AccessibilityManager'; import { Linkifier } from './Linkifier'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils, type IEvent } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; import { MutableDisposable, toDisposable } from 'common/Lifecycle'; @@ -135,15 +135,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { public readonly onBell = this._onBell.event; private _onFocus = this._register(new Emitter()); - public get onFocus(): Event { return this._onFocus.event; } + public get onFocus(): IEvent { return this._onFocus.event; } private _onBlur = this._register(new Emitter()); - public get onBlur(): Event { return this._onBlur.event; } + public get onBlur(): IEvent { return this._onBlur.event; } private _onA11yCharEmitter = this._register(new Emitter()); - public get onA11yChar(): Event { return this._onA11yCharEmitter.event; } + public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } private _onA11yTabEmitter = this._register(new Emitter()); - public get onA11yTab(): Event { return this._onA11yTabEmitter.event; } + public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } private _onWillOpen = this._register(new Emitter()); - public get onWillOpen(): Event { return this._onWillOpen.event; } + public get onWillOpen(): IEvent { return this._onWillOpen.event; } private readonly _onDimensionsChange = this._register(new Emitter()); public readonly onDimensionsChange = this._onDimensionsChange.event; @@ -187,10 +187,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._register(this._inputHandler.onRequestReset(() => this.reset())); this._register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); this._register(this._inputHandler.onColor((event) => this._handleColorEvent(event))); - this._register(Event.forward(this._inputHandler.onCursorMove, this._onCursorMove)); - this._register(Event.forward(this._inputHandler.onTitleChange, this._onTitleChange)); - this._register(Event.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); - this._register(Event.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove)); + this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange)); + this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); + this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); // Setup listeners this._register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); @@ -566,7 +566,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this.textarea!.focus(); this.textarea!.select(); })); - this._register(Event.any( + this._register(EventUtils.any( this._onScroll.event, this._inputHandler.onScroll )(() => { diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 2d83ae53..76163bad 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -18,7 +18,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { css } from 'common/Color'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; -import { Emitter, type Event } from 'common/Event'; +import { Emitter, type IEvent } from 'common/Event'; export class TestTerminal extends CoreBrowserTerminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } @@ -30,24 +30,24 @@ export class TestTerminal extends CoreBrowserTerminal { } export class MockTerminal implements ITerminal { - public onBlur!: Event; - public onFocus!: Event; - public onA11yChar!: Event; - public onWriteParsed!: Event; - public onA11yTab!: Event; - public onCursorMove!: Event; - public onLineFeed!: Event; - public onSelectionChange!: Event; - public onData!: Event; - public onBinary!: Event; - public onTitleChange!: Event; - public onBell!: Event; - public onScroll!: Event; - public onWillOpen!: Event; - public onKey!: Event<{ key: string, domEvent: KeyboardEvent }>; - public onRender!: Event<{ start: number, end: number }>; - public onResize!: Event<{ cols: number, rows: number }>; - public onDimensionsChange!: Event; + public onBlur!: IEvent; + public onFocus!: IEvent; + public onA11yChar!: IEvent; + public onWriteParsed!: IEvent; + public onA11yTab!: IEvent; + public onCursorMove!: IEvent; + public onLineFeed!: IEvent; + public onSelectionChange!: IEvent; + public onData!: IEvent; + public onBinary!: IEvent; + public onTitleChange!: IEvent; + public onBell!: IEvent; + public onScroll!: IEvent; + public onWillOpen!: IEvent; + public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>; + public onRender!: IEvent<{ start: number, end: number }>; + public onResize!: IEvent<{ cols: number, rows: number }>; + public onDimensionsChange!: IEvent; public dimensions: IRenderDimensionsApi | undefined; public markers!: IMarker[]; public linkifier: ILinkifier2 | undefined; @@ -273,9 +273,9 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { - public onRequestRedraw!: Event; - public onCanvasResize!: Event<{ width: number, height: number }>; - public onRender!: Event<{ start: number, end: number }>; + public onRequestRedraw!: IEvent; + public onCanvasResize!: IEvent<{ width: number, height: number }>; + public onRender!: IEvent<{ start: number, end: number }>; public dispose(): void { throw new Error('Method not implemented.'); } @@ -378,7 +378,7 @@ export class MockCoreBrowserService implements ICoreBrowserService { export class MockCharSizeService implements ICharSizeService { public serviceBrand: undefined; public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } - public onCharSizeChange: Event = new Emitter().event; + public onCharSizeChange: IEvent = new Emitter().event; constructor(public width: number, public height: number) {} public measure(): void {} } @@ -396,10 +396,10 @@ export class MockMouseService implements IMouseService { export class MockRenderService implements IRenderService { public serviceBrand: undefined; - public onDimensionsChange: Event = new Emitter().event; - public onRenderedViewportChange: Event<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event; - public onRender: Event<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event; - public onRefreshRequest: Event<{ start: number, end: number}> = new Emitter<{ start: number, end: number }>().event; + public onDimensionsChange: IEvent = new Emitter().event; + public onRenderedViewportChange: IEvent<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event; + public onRender: IEvent<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event; + public onRefreshRequest: IEvent<{ start: number, end: number}> = new Emitter<{ start: number, end: number }>().event; public dimensions: IRenderDimensions = createRenderDimensions(); public refreshRows(start: number, end: number): void { throw new Error('Method not implemented.'); diff --git a/src/browser/Types.ts b/src/browser/Types.ts index 5c840bff..f54e488c 100644 --- a/src/browser/Types.ts +++ b/src/browser/Types.ts @@ -7,7 +7,7 @@ import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types' import { IBuffer } from 'common/buffer/Types'; import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm'; import { channels, css } from 'common/Color'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; /** * A portion of the public API that are implemented identially internally and simply passed through. @@ -23,12 +23,12 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal { readonly dimensions: IRenderDimensionsApi | undefined; - onBlur: Event; - onFocus: Event; - onDimensionsChange: Event; - onA11yChar: Event; - onA11yTab: Event; - onWillOpen: Event; + onBlur: IEvent; + onFocus: IEvent; + onDimensionsChange: IEvent; + onA11yChar: IEvent; + onA11yTab: IEvent; + onWillOpen: IEvent; cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | void; } @@ -101,7 +101,7 @@ export interface IPartialColorSet { export interface IViewport extends IDisposable { scrollBarWidth: number; - readonly onRequestScrollLines: Event<{ amount: number, suppressScrollEvent: boolean }>; + readonly onRequestScrollLines: IEvent<{ amount: number, suppressScrollEvent: boolean }>; syncScrollArea(immediate?: boolean, force?: boolean): void; getLinesScrolled(ev: WheelEvent): number; getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement }; @@ -131,8 +131,8 @@ export interface ILinkWithState { } export interface ILinkifier2 extends IDisposable { - onShowLinkUnderline: Event; - onHideLinkUnderline: Event; + onShowLinkUnderline: IEvent; + onHideLinkUnderline: IEvent; readonly currentLink: ILinkWithState | undefined; } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 33442976..2e1d8861 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -11,7 +11,7 @@ import { CoreMouseEventType } from 'common/Types'; import { addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils } from 'common/Event'; import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable'; import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch'; @@ -71,7 +71,7 @@ export class Viewport extends Disposable { })); this._scrollableElement.setScrollDimensions({ height: 0, scrollHeight: 0 }); - this._register(Event.runAndSubscribe(themeService.onChangeColors, () => { + this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => { element.style.backgroundColor = themeService.colors.background.css; this._scrollableElement.getDomNode().style.backgroundColor = themeService.colors.background.css; })); @@ -81,7 +81,7 @@ export class Viewport extends Disposable { this._styleElement = coreBrowserService.mainDocument.createElement('style'); screenElement.appendChild(this._styleElement); this._register(toDisposable(() => this._styleElement.remove())); - this._register(Event.runAndSubscribe(themeService.onChangeColors, () => { + this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => { this._styleElement.textContent = [ `.xterm .xterm-scrollable-element > .scrollbar > .slider {`, ` background: ${themeService.colors.scrollbarSliderBackground.css};`, diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 16006905..6b4cdee2 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -13,7 +13,7 @@ import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; import { ParserApi } from 'common/public/ParserApi'; import { UnicodeApi } from 'common/public/UnicodeApi'; import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; /** * The set of options that only have an effect when set in the Terminal constructor. @@ -68,19 +68,19 @@ export class Terminal extends Disposable implements ITerminalApi { } } - public get onBell(): Event { return this._core.onBell; } - public get onBinary(): Event { return this._core.onBinary; } - public get onCursorMove(): Event { return this._core.onCursorMove; } - public get onData(): Event { return this._core.onData; } - public get onKey(): Event<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } - public get onLineFeed(): Event { return this._core.onLineFeed; } - public get onRender(): Event<{ start: number, end: number }> { return this._core.onRender; } - public get onResize(): Event<{ cols: number, rows: number }> { return this._core.onResize; } - public get onScroll(): Event { return this._core.onScroll; } - public get onSelectionChange(): Event { return this._core.onSelectionChange; } - public get onTitleChange(): Event { return this._core.onTitleChange; } - public get onWriteParsed(): Event { return this._core.onWriteParsed; } - public get onDimensionsChange(): Event { return this._core.onDimensionsChange; } + public get onBell(): IEvent { return this._core.onBell; } + public get onBinary(): IEvent { return this._core.onBinary; } + public get onCursorMove(): IEvent { return this._core.onCursorMove; } + public get onData(): IEvent { return this._core.onData; } + public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } + public get onLineFeed(): IEvent { return this._core.onLineFeed; } + public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; } + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get onScroll(): IEvent { return this._core.onScroll; } + public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } + public get onTitleChange(): IEvent { return this._core.onTitleChange; } + public get onWriteParsed(): IEvent { return this._core.onWriteParsed; } + public get onDimensionsChange(): IEvent { return this._core.onDimensionsChange; } public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { diff --git a/src/browser/renderer/shared/Types.ts b/src/browser/renderer/shared/Types.ts index 05f462a7..f86cd4fe 100644 --- a/src/browser/renderer/shared/Types.ts +++ b/src/browser/renderer/shared/Types.ts @@ -6,7 +6,7 @@ import { Terminal } from '@xterm/xterm'; import { ITerminal } from 'browser/Types'; import { IDisposable } from 'common/Types'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; export interface IDimensions { width: number; @@ -57,7 +57,7 @@ export interface IRenderer extends IDisposable { * Fires when the renderer is requesting to be redrawn on the next animation * frame but is _not_ a result of content changing (eg. selection changes). */ - readonly onRequestRedraw: Event; + readonly onRequestRedraw: IEvent; dispose(): void; handleDevicePixelRatioChange(): void; diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts index 562793f2..4e5d8b71 100644 --- a/src/browser/services/CoreBrowserService.ts +++ b/src/browser/services/CoreBrowserService.ts @@ -4,7 +4,7 @@ */ import { ICoreBrowserService } from './Services'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils } from 'common/Event'; import { addDisposableListener } from 'vs/base/browser/dom'; import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; @@ -29,7 +29,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic // Monitor device pixel ratio this._register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w))); - this._register(Event.forward(this._screenDprMonitor.onDprChange, this._onDprChange)); + this._register(EventUtils.forward(this._screenDprMonitor.onDprChange, this._onDprChange)); this._register(addDisposableListener(this._textarea, 'focus', () => this._isFocused = true)); this._register(addDisposableListener(this._textarea, 'blur', () => this._isFocused = false)); diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index af9211df..1c82b28b 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -8,7 +8,7 @@ import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { AllColorIndex, IDisposable, IKeyboardResult } from 'common/Types'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { @@ -18,7 +18,7 @@ export interface ICharSizeService { readonly height: number; readonly hasValidSize: boolean; - readonly onCharSizeChange: Event; + readonly onCharSizeChange: IEvent; measure(): void; } @@ -29,8 +29,8 @@ export interface ICoreBrowserService { readonly isFocused: boolean; - readonly onDprChange: Event; - readonly onWindowChange: Event; + readonly onDprChange: IEvent; + readonly onWindowChange: IEvent; /** * Gets or sets the parent window that the terminal is rendered into. DOM and rendering APIs (e.g. @@ -61,17 +61,17 @@ export const IRenderService = createDecorator('RenderService'); export interface IRenderService extends IDisposable { serviceBrand: undefined; - onDimensionsChange: Event; + onDimensionsChange: IEvent; /** * Fires when buffer changes are rendered. This does not fire when only cursor * or selections are rendered. */ - onRenderedViewportChange: Event<{ start: number, end: number }>; + onRenderedViewportChange: IEvent<{ start: number, end: number }>; /** * Fires on render */ - onRender: Event<{ start: number, end: number }>; - onRefreshRequest: Event<{ start: number, end: number }>; + onRender: IEvent<{ start: number, end: number }>; + onRefreshRequest: IEvent<{ start: number, end: number }>; dimensions: IRenderDimensions; @@ -101,10 +101,10 @@ export interface ISelectionService { readonly selectionStart: [number, number] | undefined; readonly selectionEnd: [number, number] | undefined; - readonly onLinuxMouseSelection: Event; - readonly onRequestRedraw: Event; - readonly onRequestScrollLines: Event; - readonly onSelectionChange: Event; + readonly onLinuxMouseSelection: IEvent; + readonly onRequestRedraw: IEvent; + readonly onRequestScrollLines: IEvent; + readonly onSelectionChange: IEvent; disable(): void; enable(): void; @@ -136,7 +136,7 @@ export interface IThemeService { readonly colors: ReadonlyColorSet; - readonly onChangeColors: Event; + readonly onChangeColors: IEvent; restoreColor(slot?: AllColorIndex): void; /** diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index cff8558e..d630013c 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -37,7 +37,7 @@ import { IBufferSet } from 'common/buffer/Types'; import { InputHandler } from 'common/InputHandler'; import { WriteBuffer } from 'common/input/WriteBuffer'; import { OscLinkService } from 'common/services/OscLinkService'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils, type IEvent } from 'common/Event'; import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; // Only trigger this warning a single time per session @@ -78,7 +78,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { */ protected _onScrollApi?: Emitter; protected _onScroll = this._register(new Emitter()); - public get onScroll(): Event { + public get onScroll(): IEvent { if (!this._onScrollApi) { this._onScrollApi = this._register(new Emitter()); this._onScroll.event(ev => { @@ -125,12 +125,12 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { // Register input handler and handle/forward events this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService)); - this._register(Event.forward(this._inputHandler.onLineFeed, this._onLineFeed)); + this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed)); // Setup listeners - this._register(Event.forward(this._bufferService.onResize, this._onResize)); - this._register(Event.forward(this.coreService.onData, this._onData)); - this._register(Event.forward(this.coreService.onBinary, this._onBinary)); + this._register(EventUtils.forward(this._bufferService.onResize, this._onResize)); + this._register(EventUtils.forward(this.coreService.onData, this._onData)); + this._register(EventUtils.forward(this.coreService.onBinary, this._onBinary)); this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true))); this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput())); this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange())); @@ -140,7 +140,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { })); // Setup WriteBuffer this._writeBuffer = this._register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult))); - this._register(Event.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed)); + this._register(EventUtils.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed)); } public write(data: string | Uint8Array, callback?: () => void): void { diff --git a/src/common/Event.ts b/src/common/Event.ts index 44146e76..9ea39b9c 100644 --- a/src/common/Event.ts +++ b/src/common/Event.ts @@ -68,7 +68,7 @@ export class Emitter { } } -export namespace Event { +export namespace EventUtils { export function forward(from: IEvent, to: Emitter): IDisposable { return from(e => to.fire(e)); } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index cbe4fe58..137174e8 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -12,14 +12,14 @@ import { BufferSet } from 'common/buffer/BufferSet'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData, IDisposable } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; import { IDecorationOptions, IDecoration } from '@xterm/xterm'; -import { Emitter, type Event } from 'common/Event'; +import { Emitter, type IEvent } from 'common/Event'; export class MockBufferService implements IBufferService { public serviceBrand: any; public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; - public onResize: Event = new Emitter().event; - public onScroll: Event = new Emitter().event; + public onResize: IEvent = new Emitter().event; + public onScroll: IEvent = new Emitter().event; private readonly _onScroll = new Emitter(); public isUserScrolling: boolean = false; constructor( @@ -67,7 +67,7 @@ export class MockCoreMouseService implements ICoreMouseService { public addProtocol(name: string): void { } public reset(): void { } public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } - public onProtocolChange: Event = new Emitter().event; + public onProtocolChange: IEvent = new Emitter().event; public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } @@ -122,10 +122,10 @@ export class MockCoreService implements ICoreService { mainStack: [] as number[], altStack: [] as number[] }; - public onData: Event = new Emitter().event; - public onUserInput: Event = new Emitter().event; - public onBinary: Event = new Emitter().event; - public onRequestScrollToBottom: Event = new Emitter().event; + public onData: IEvent = new Emitter().event; + public onUserInput: IEvent = new Emitter().event; + public onBinary: IEvent = new Emitter().event; + public onRequestScrollToBottom: IEvent = new Emitter().event; public reset(): void { } public triggerDataEvent(data: string, wasUserInput?: boolean): void { } public triggerBinaryEvent(data: string): void { } @@ -145,7 +145,7 @@ export class MockOptionsService implements IOptionsService { public serviceBrand: any; public readonly rawOptions: Required = clone(DEFAULT_OPTIONS); public options: Required = this.rawOptions; - public onOptionChange: Event = new Emitter().event; + public onOptionChange: IEvent = new Emitter().event; constructor(testOptions?: Partial) { if (testOptions) { for (const key of Object.keys(testOptions)) { @@ -197,7 +197,7 @@ export class MockUnicodeService implements IUnicodeService { } public versions: string[] = []; public activeVersion: string = ''; - public onChange: Event = new Emitter().event; + public onChange: IEvent = new Emitter().event; public wcwidth = (codepoint: number): UnicodeCharWidth => this._provider.wcwidth(codepoint); public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { let width = this.wcwidth(codepoint); diff --git a/src/common/Types.ts b/src/common/Types.ts index aeeee5b2..ec10ed55 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -9,7 +9,7 @@ import { IBufferSet } from 'common/buffer/Types'; import { IParams } from 'common/parser/Types'; import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from '@xterm/xterm'; -import type { Emitter, Event } from 'common/Event'; +import type { Emitter, IEvent } from 'common/Event'; export interface ICoreTerminal { coreMouseService: ICoreMouseService; @@ -68,11 +68,11 @@ export interface ICircularList { isFull: boolean; onDeleteEmitter: Emitter; - onDelete: Event; + onDelete: IEvent; onInsertEmitter: Emitter; - onInsert: Event; + onInsert: IEvent; onTrimEmitter: Emitter; - onTrim: Event; + onTrim: IEvent; get(index: number): T | undefined; set(index: number, value: T): void; @@ -258,7 +258,7 @@ export interface IMarker extends IDisposable { readonly id: number; readonly isDisposed: boolean; readonly line: number; - onDispose: Event; + onDispose: IEvent; } export interface IModes { insertMode: boolean; @@ -467,7 +467,7 @@ export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestor * Calls the parser and handles actions generated by the parser. */ export interface IInputHandler { - onTitleChange: Event; + onTitleChange: IEvent; parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise; print(data: Uint32Array, start: number, end: number): void; diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts index b6e83586..85dd68ee 100644 --- a/src/common/buffer/Types.ts +++ b/src/common/buffer/Types.ts @@ -4,7 +4,7 @@ */ import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker, ICharset, IDisposable } from 'common/Types'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; // BufferIndex denotes a position in the buffer: [rowIndex, colIndex] export type BufferIndex = [number, number]; @@ -46,7 +46,7 @@ export interface IBufferSet extends IDisposable { normal: IBuffer; active: IBuffer; - onBufferActivate: Event<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; + onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; activateNormalBuffer(): void; activateAltBuffer(fillAttr?: IAttributeData): void; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 737dcd0d..30ad6048 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -7,7 +7,7 @@ import { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, ty import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from 'common/Types'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; -import type { Emitter, Event } from 'common/Event'; +import type { Emitter, IEvent } from 'common/Event'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { @@ -18,8 +18,8 @@ export interface IBufferService { readonly buffer: IBuffer; readonly buffers: IBufferSet; isUserScrolling: boolean; - onResize: Event; - onScroll: Event; + onResize: IEvent; + onScroll: IEvent; scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; resize(cols: number, rows: number): void; @@ -59,7 +59,7 @@ export interface ICoreMouseService { /** * Event to announce changes in mouse tracking. */ - onProtocolChange: Event; + onProtocolChange: IEvent; /** * Human readable version of mouse events. @@ -87,10 +87,10 @@ export interface ICoreService { readonly decPrivateModes: IDecPrivateModes; readonly kittyKeyboard: IKittyKeyboardState; - readonly onData: Event; - readonly onUserInput: Event; - readonly onBinary: Event; - readonly onRequestScrollToBottom: Event; + readonly onData: IEvent; + readonly onUserInput: IEvent; + readonly onBinary: IEvent; + readonly onRequestScrollToBottom: IEvent; reset(): void; @@ -201,7 +201,7 @@ export interface IOptionsService { /** * Adds an event listener for when any option changes. */ - readonly onOptionChange: Event; + readonly onOptionChange: IEvent; /** * Adds an event listener for when a specific option changes, this is a convenience method that is @@ -364,7 +364,7 @@ export interface IUnicodeService { /** Currently active version. */ activeVersion: string; /** Event triggered, when activate version changed. */ - readonly onChange: Event; + readonly onChange: IEvent; /** * Unicode version dependent @@ -389,8 +389,8 @@ export const IDecorationService = createDecorator('Decoratio export interface IDecorationService extends IDisposable { serviceBrand: undefined; readonly decorations: IterableIterator; - readonly onDecorationRegistered: Event; - readonly onDecorationRemoved: Event; + readonly onDecorationRegistered: IEvent; + readonly onDecorationRemoved: IEvent; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; reset(): void; /** diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 52618e86..cc8b152b 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -25,7 +25,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBuffer } from 'common/buffer/Types'; import { CoreTerminal } from 'common/CoreTerminal'; import { IMarker, ITerminalOptions } from 'common/Types'; -import { Emitter, Event } from 'common/Event'; +import { Emitter, EventUtils } from 'common/Event'; export class Terminal extends CoreTerminal { private readonly _onBell = this._register(new Emitter()); @@ -49,11 +49,11 @@ export class Terminal extends CoreTerminal { // Setup InputHandler listeners this._register(this._inputHandler.onRequestBell(() => this.bell())); this._register(this._inputHandler.onRequestReset(() => this.reset())); - this._register(Event.forward(this._inputHandler.onCursorMove, this._onCursorMove)); - this._register(Event.forward(this._inputHandler.onTitleChange, this._onTitleChange)); - this._register(Event.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); - this._register(Event.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); - this._register(Event.forward(Event.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender)); + this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove)); + this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange)); + this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); + this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + this._register(EventUtils.forward(EventUtils.map(this._inputHandler.onRequestRefreshRows, e => ({ start: e?.start ?? 0, end: e?.end ?? this.rows - 1 })), this._onRender)); } /** diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 17b143fa..cdb94180 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -11,7 +11,7 @@ import { Terminal as TerminalCore } from 'headless/Terminal'; import { AddonManager } from 'common/public/AddonManager'; import { ITerminalOptions } from 'common/Types'; import { Disposable } from 'common/Lifecycle'; -import type { Event } from 'common/Event'; +import type { IEvent } from 'common/Event'; /** * The set of options that only have an effect when set in the Terminal constructor. */ @@ -72,16 +72,16 @@ export class Terminal extends Disposable implements ITerminalApi { } } - public get onBell(): Event { return this._core.onBell; } - public get onBinary(): Event { return this._core.onBinary; } - public get onCursorMove(): Event { return this._core.onCursorMove; } - public get onData(): Event { return this._core.onData; } - public get onLineFeed(): Event { return this._core.onLineFeed; } - public get onRender(): Event<{ start: number, end: number }> { return this._core.onRender; } - public get onResize(): Event<{ cols: number, rows: number }> { return this._core.onResize; } - public get onScroll(): Event { return this._core.onScroll; } - public get onTitleChange(): Event { return this._core.onTitleChange; } - public get onWriteParsed(): Event { return this._core.onWriteParsed; } + public get onBell(): IEvent { return this._core.onBell; } + public get onBinary(): IEvent { return this._core.onBinary; } + public get onCursorMove(): IEvent { return this._core.onCursorMove; } + public get onData(): IEvent { return this._core.onData; } + public get onLineFeed(): IEvent { return this._core.onLineFeed; } + public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; } + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get onScroll(): IEvent { return this._core.onScroll; } + public get onTitleChange(): IEvent { return this._core.onTitleChange; } + public get onWriteParsed(): IEvent { return this._core.onWriteParsed; } public get parser(): IParser { if (!this._parser) { From b04d816d8a2c31b318c6e040c177b4ec1cb6f17f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:32:49 -0800 Subject: [PATCH 22/31] Undo changes to headless/package.json --- headless/package.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/headless/package.json b/headless/package.json index fda5eae6..6ca8d387 100644 --- a/headless/package.json +++ b/headless/package.json @@ -3,13 +3,15 @@ "description": "A headless terminal component that runs in Node.js", "version": "6.0.0", "main": "lib-headless/xterm-headless.js", - "module": "lib/xterm.mjs", + "module": "lib-headless/xterm-headless.mjs", "types": "typings/xterm-headless.d.ts", + "exports": { + "types": "./typings/xterm-headless.d.ts", + "import": "./lib-headless/xterm-headless.mjs", + "require": "./lib-headless/xterm-headless.js" + }, "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", - "workspaces": [ - "addons/*" - ], "keywords": [ "cli", "command-line", @@ -25,4 +27,4 @@ "webgl", "xterm" ] -} \ No newline at end of file +} From 77fa03d11c5d53a1e01587cc9ba527bcd9ac4b74 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:53:02 -0800 Subject: [PATCH 23/31] Remove unused TypedArrayUtils.ts --- src/common/TypedArrayUtils.test.ts | 24 ------------------------ src/common/TypedArrayUtils.ts | 17 ----------------- 2 files changed, 41 deletions(-) delete mode 100644 src/common/TypedArrayUtils.test.ts delete mode 100644 src/common/TypedArrayUtils.ts diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts deleted file mode 100644 index 6390854b..00000000 --- a/src/common/TypedArrayUtils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ -import { assert } from 'chai'; -import { concat } from 'common/TypedArrayUtils'; - -type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; - -function deepEquals(a: TypedArray, b: TypedArray): void { - assert.equal(a.length, b.length); - for (let i = 0; i < a.length; ++i) { - assert.equal(a[i], b[i]); - } -} - -describe('typed array convenience functions', () => { - it('concat', () => { - const a = new Uint8Array([1, 2, 3, 4, 5]); - const b = new Uint8Array([6, 7, 8, 9, 0]); - const merged = concat(a, b); - deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); - }); -}); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts deleted file mode 100644 index f3bacd50..00000000 --- a/src/common/TypedArrayUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; - -/** - * Concat two typed arrays `a` and `b`. - * Returns a new typed array. - */ -export function concat(a: T, b: T): T { - const result = new (a.constructor as any)(a.length + b.length); - result.set(a); - result.set(b, a.length); - return result; -} From 9ffd16aa9b54961629cc6780fb09050fb83a105f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:55:50 -0800 Subject: [PATCH 24/31] Use const enum in EscapeSequences They'll be inlined and only the used ones will show in bundle --- src/common/data/EscapeSequences.ts | 141 +++++++++++++++-------------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/src/common/data/EscapeSequences.ts b/src/common/data/EscapeSequences.ts index 0e034620..2f60125f 100644 --- a/src/common/data/EscapeSequences.ts +++ b/src/common/data/EscapeSequences.ts @@ -7,147 +7,148 @@ * C0 control codes * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes */ -export namespace C0 { +export const enum C0 { /** Null (Caret = ^@, C = \0) */ - export const NUL = '\x00'; + NUL = '\x00', /** Start of Heading (Caret = ^A) */ - export const SOH = '\x01'; + SOH = '\x01', /** Start of Text (Caret = ^B) */ - export const STX = '\x02'; + STX = '\x02', /** End of Text (Caret = ^C) */ - export const ETX = '\x03'; + ETX = '\x03', /** End of Transmission (Caret = ^D) */ - export const EOT = '\x04'; + EOT = '\x04', /** Enquiry (Caret = ^E) */ - export const ENQ = '\x05'; + ENQ = '\x05', /** Acknowledge (Caret = ^F) */ - export const ACK = '\x06'; + ACK = '\x06', /** Bell (Caret = ^G, C = \a) */ - export const BEL = '\x07'; + BEL = '\x07', /** Backspace (Caret = ^H, C = \b) */ - export const BS = '\x08'; + BS = '\x08', /** Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t) */ - export const HT = '\x09'; + HT = '\x09', /** Line Feed (Caret = ^J, C = \n) */ - export const LF = '\x0a'; + LF = '\x0a', /** Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v) */ - export const VT = '\x0b'; + VT = '\x0b', /** Form Feed (Caret = ^L, C = \f) */ - export const FF = '\x0c'; + FF = '\x0c', /** Carriage Return (Caret = ^M, C = \r) */ - export const CR = '\x0d'; + CR = '\x0d', /** Shift Out (Caret = ^N) */ - export const SO = '\x0e'; + SO = '\x0e', /** Shift In (Caret = ^O) */ - export const SI = '\x0f'; + SI = '\x0f', /** Data Link Escape (Caret = ^P) */ - export const DLE = '\x10'; + DLE = '\x10', /** Device Control One (XON) (Caret = ^Q) */ - export const DC1 = '\x11'; + DC1 = '\x11', /** Device Control Two (Caret = ^R) */ - export const DC2 = '\x12'; + DC2 = '\x12', /** Device Control Three (XOFF) (Caret = ^S) */ - export const DC3 = '\x13'; + DC3 = '\x13', /** Device Control Four (Caret = ^T) */ - export const DC4 = '\x14'; + DC4 = '\x14', /** Negative Acknowledge (Caret = ^U) */ - export const NAK = '\x15'; + NAK = '\x15', /** Synchronous Idle (Caret = ^V) */ - export const SYN = '\x16'; + SYN = '\x16', /** End of Transmission Block (Caret = ^W) */ - export const ETB = '\x17'; + ETB = '\x17', /** Cancel (Caret = ^X) */ - export const CAN = '\x18'; + CAN = '\x18', /** End of Medium (Caret = ^Y) */ - export const EM = '\x19'; + EM = '\x19', /** Substitute (Caret = ^Z) */ - export const SUB = '\x1a'; + SUB = '\x1a', /** Escape (Caret = ^[, C = \e) */ - export const ESC = '\x1b'; + ESC = '\x1b', /** File Separator (Caret = ^\) */ - export const FS = '\x1c'; + FS = '\x1c', /** Group Separator (Caret = ^]) */ - export const GS = '\x1d'; + GS = '\x1d', /** Record Separator (Caret = ^^) */ - export const RS = '\x1e'; + RS = '\x1e', /** Unit Separator (Caret = ^_) */ - export const US = '\x1f'; + US = '\x1f', /** Space */ - export const SP = '\x20'; + SP = '\x20', /** Delete (Caret = ^?) */ - export const DEL = '\x7f'; + DEL = '\x7f' } /** * C1 control codes * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes */ -export namespace C1 { +export const enum C1 { /** padding character */ - export const PAD = '\x80'; + PAD = '\x80', /** High Octet Preset */ - export const HOP = '\x81'; + HOP = '\x81', /** Break Permitted Here */ - export const BPH = '\x82'; + BPH = '\x82', /** No Break Here */ - export const NBH = '\x83'; + NBH = '\x83', /** Index */ - export const IND = '\x84'; + IND = '\x84', /** Next Line */ - export const NEL = '\x85'; + NEL = '\x85', /** Start of Selected Area */ - export const SSA = '\x86'; + SSA = '\x86', /** End of Selected Area */ - export const ESA = '\x87'; + ESA = '\x87', /** Horizontal Tabulation Set */ - export const HTS = '\x88'; + HTS = '\x88', /** Horizontal Tabulation With Justification */ - export const HTJ = '\x89'; + HTJ = '\x89', /** Vertical Tabulation Set */ - export const VTS = '\x8a'; + VTS = '\x8a', /** Partial Line Down */ - export const PLD = '\x8b'; + PLD = '\x8b', /** Partial Line Up */ - export const PLU = '\x8c'; + PLU = '\x8c', /** Reverse Index */ - export const RI = '\x8d'; + RI = '\x8d', /** Single-Shift 2 */ - export const SS2 = '\x8e'; + SS2 = '\x8e', /** Single-Shift 3 */ - export const SS3 = '\x8f'; + SS3 = '\x8f', /** Device Control String */ - export const DCS = '\x90'; + DCS = '\x90', /** Private Use 1 */ - export const PU1 = '\x91'; + PU1 = '\x91', /** Private Use 2 */ - export const PU2 = '\x92'; + PU2 = '\x92', /** Set Transmit State */ - export const STS = '\x93'; + STS = '\x93', /** Destructive backspace, intended to eliminate ambiguity about meaning of BS. */ - export const CCH = '\x94'; + CCH = '\x94', /** Message Waiting */ - export const MW = '\x95'; + MW = '\x95', /** Start of Protected Area */ - export const SPA = '\x96'; + SPA = '\x96', /** End of Protected Area */ - export const EPA = '\x97'; + EPA = '\x97', /** Start of String */ - export const SOS = '\x98'; + SOS = '\x98', /** Single Graphic Character Introducer */ - export const SGCI = '\x99'; + SGCI = '\x99', /** Single Character Introducer */ - export const SCI = '\x9a'; + SCI = '\x9a', /** Control Sequence Introducer */ - export const CSI = '\x9b'; + CSI = '\x9b', /** String Terminator */ - export const ST = '\x9c'; + ST = '\x9c', /** Operating System Command */ - export const OSC = '\x9d'; + OSC = '\x9d', /** Privacy Message */ - export const PM = '\x9e'; + PM = '\x9e', /** Application Program Command */ - export const APC = '\x9f'; + APC = '\x9f' } -export namespace C1_ESCAPED { - export const ST = `${C0.ESC}\\`; + +export const enum C1_ESCAPED { + ST = '\x1b\\' } From d35481c3f97128ba1225c90f05accf1fc8f84e13 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:07:55 -0800 Subject: [PATCH 25/31] Move win32 input mode into a class to avoid init cost --- src/browser/services/KeyboardService.ts | 15 +- src/common/input/Win32InputMode.test.ts | 12 +- src/common/input/Win32InputMode.ts | 507 ++++++++++++------------ 3 files changed, 268 insertions(+), 266 deletions(-) diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index 478a262e..afdde381 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -6,7 +6,7 @@ import { IKeyboardService } from 'browser/services/Services'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard'; -import { evaluateKeyboardEventWin32 } from 'common/input/Win32InputMode'; +import { Win32InputMode } from 'common/input/Win32InputMode'; import { isMac } from 'common/Platform'; import { ICoreService, IOptionsService } from 'common/services/Services'; import { IKeyboardResult } from 'common/Types'; @@ -14,16 +14,25 @@ import { IKeyboardResult } from 'common/Types'; export class KeyboardService implements IKeyboardService { public serviceBrand: undefined; + private _win32InputMode: Win32InputMode | undefined; + constructor( @ICoreService private readonly _coreService: ICoreService, @IOptionsService private readonly _optionsService: IOptionsService ) { } + private _getWin32InputMode(): Win32InputMode { + if (!this._win32InputMode) { + this._win32InputMode = new Win32InputMode(); + } + return this._win32InputMode; + } + public evaluateKeyDown(event: KeyboardEvent): IKeyboardResult { // Win32 input mode takes priority (most raw) if (this.useWin32InputMode) { - return evaluateKeyboardEventWin32(event, true); + return this._getWin32InputMode().evaluateKeyboardEvent(event, true); } const kittyFlags = this._coreService.kittyKeyboard.flags; return this.useKitty @@ -34,7 +43,7 @@ export class KeyboardService implements IKeyboardService { public evaluateKeyUp(event: KeyboardEvent): IKeyboardResult | undefined { // Win32 input mode sends key up events if (this.useWin32InputMode) { - return evaluateKeyboardEventWin32(event, false); + return this._getWin32InputMode().evaluateKeyboardEvent(event, false); } const kittyFlags = this._coreService.kittyKeyboard.flags; if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) { diff --git a/src/common/input/Win32InputMode.test.ts b/src/common/input/Win32InputMode.test.ts index e5dc98b4..d0f33359 100644 --- a/src/common/input/Win32InputMode.test.ts +++ b/src/common/input/Win32InputMode.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { evaluateKeyboardEventWin32, Win32ControlKeyState } from 'common/input/Win32InputMode'; +import { Win32InputMode, Win32ControlKeyState } from 'common/input/Win32InputMode'; import { IKeyboardEvent, KeyboardResultType } from 'common/Types'; type EventOpts = Partial; @@ -18,18 +18,20 @@ const parse = (seq: string) => { return m ? { vk: +m[1], sc: +m[2], uc: +m[3], kd: +m[4], cs: +m[5], rc: +m[6] } : null; }; +const win32 = new Win32InputMode(); + const test = (opts: EventOpts, isDown: boolean, check: (p: ReturnType) => void) => { - const result = evaluateKeyboardEventWin32(ev(opts), isDown); + const result = win32.evaluateKeyboardEvent(ev(opts), isDown); const parsed = parse(result.key!); assert.ok(parsed); check(parsed); }; describe('Win32InputMode', () => { - describe('evaluateKeyboardEventWin32', () => { + describe('evaluateKeyboardEvent', () => { describe('basic key encoding', () => { it('letter key press', () => { - const result = evaluateKeyboardEventWin32(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + const result = win32.evaluateKeyboardEvent(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); assert.strictEqual(result.type, KeyboardResultType.SEND_KEY); assert.strictEqual(result.cancel, true); const p = parse(result.key!); @@ -135,7 +137,7 @@ describe('Win32InputMode', () => { describe('sequence format', () => { it('valid CSI format', () => { - const result = evaluateKeyboardEventWin32(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); + const result = win32.evaluateKeyboardEvent(ev({ code: 'KeyA', key: 'a', keyCode: 65 }), true); assert.ok(result.key?.startsWith('\x1b[') && result.key.endsWith('_')); assert.strictEqual(result.key?.slice(2, -1).split(';').length, 6); }); diff --git a/src/common/input/Win32InputMode.ts b/src/common/input/Win32InputMode.ts index 6818bc9f..72907836 100644 --- a/src/common/input/Win32InputMode.ts +++ b/src/common/input/Win32InputMode.ts @@ -33,274 +33,265 @@ export const enum Win32ControlKeyState { } /** - * Mapping from browser KeyboardEvent.code to Win32 virtual key codes. - * Based on https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + * Win32 input mode handler. Lookup tables are only initialized when this class + * is instantiated, reducing bundle size for environments that don't use this mode. */ -const CODE_TO_VK: { [code: string]: number } = { - // Letters - 'KeyA': 0x41, 'KeyB': 0x42, 'KeyC': 0x43, 'KeyD': 0x44, 'KeyE': 0x45, - 'KeyF': 0x46, 'KeyG': 0x47, 'KeyH': 0x48, 'KeyI': 0x49, 'KeyJ': 0x4A, - 'KeyK': 0x4B, 'KeyL': 0x4C, 'KeyM': 0x4D, 'KeyN': 0x4E, 'KeyO': 0x4F, - 'KeyP': 0x50, 'KeyQ': 0x51, 'KeyR': 0x52, 'KeyS': 0x53, 'KeyT': 0x54, - 'KeyU': 0x55, 'KeyV': 0x56, 'KeyW': 0x57, 'KeyX': 0x58, 'KeyY': 0x59, - 'KeyZ': 0x5A, +export class Win32InputMode { + /** + * Mapping from browser KeyboardEvent.code to Win32 virtual key codes. + * Based on https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + */ + private readonly _codeToVk: { [code: string]: number } = { + // Letters + 'KeyA': 0x41, 'KeyB': 0x42, 'KeyC': 0x43, 'KeyD': 0x44, 'KeyE': 0x45, + 'KeyF': 0x46, 'KeyG': 0x47, 'KeyH': 0x48, 'KeyI': 0x49, 'KeyJ': 0x4A, + 'KeyK': 0x4B, 'KeyL': 0x4C, 'KeyM': 0x4D, 'KeyN': 0x4E, 'KeyO': 0x4F, + 'KeyP': 0x50, 'KeyQ': 0x51, 'KeyR': 0x52, 'KeyS': 0x53, 'KeyT': 0x54, + 'KeyU': 0x55, 'KeyV': 0x56, 'KeyW': 0x57, 'KeyX': 0x58, 'KeyY': 0x59, + 'KeyZ': 0x5A, - // Digits - 'Digit0': 0x30, 'Digit1': 0x31, 'Digit2': 0x32, 'Digit3': 0x33, 'Digit4': 0x34, - 'Digit5': 0x35, 'Digit6': 0x36, 'Digit7': 0x37, 'Digit8': 0x38, 'Digit9': 0x39, + // Digits + 'Digit0': 0x30, 'Digit1': 0x31, 'Digit2': 0x32, 'Digit3': 0x33, 'Digit4': 0x34, + 'Digit5': 0x35, 'Digit6': 0x36, 'Digit7': 0x37, 'Digit8': 0x38, 'Digit9': 0x39, - // Function keys - 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, 'F6': 0x75, - 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, 'F11': 0x7A, 'F12': 0x7B, - 'F13': 0x7C, 'F14': 0x7D, 'F15': 0x7E, 'F16': 0x7F, 'F17': 0x80, 'F18': 0x81, - 'F19': 0x82, 'F20': 0x83, 'F21': 0x84, 'F22': 0x85, 'F23': 0x86, 'F24': 0x87, + // Function keys + 'F1': 0x70, 'F2': 0x71, 'F3': 0x72, 'F4': 0x73, 'F5': 0x74, 'F6': 0x75, + 'F7': 0x76, 'F8': 0x77, 'F9': 0x78, 'F10': 0x79, 'F11': 0x7A, 'F12': 0x7B, + 'F13': 0x7C, 'F14': 0x7D, 'F15': 0x7E, 'F16': 0x7F, 'F17': 0x80, 'F18': 0x81, + 'F19': 0x82, 'F20': 0x83, 'F21': 0x84, 'F22': 0x85, 'F23': 0x86, 'F24': 0x87, - // Numpad - 'Numpad0': 0x60, 'Numpad1': 0x61, 'Numpad2': 0x62, 'Numpad3': 0x63, 'Numpad4': 0x64, - 'Numpad5': 0x65, 'Numpad6': 0x66, 'Numpad7': 0x67, 'Numpad8': 0x68, 'Numpad9': 0x69, - 'NumpadMultiply': 0x6A, 'NumpadAdd': 0x6B, 'NumpadSeparator': 0x6C, - 'NumpadSubtract': 0x6D, 'NumpadDecimal': 0x6E, 'NumpadDivide': 0x6F, - 'NumpadEnter': 0x0D, // Same as Enter but with ENHANCED_KEY flag - 'NumLock': 0x90, + // Numpad + 'Numpad0': 0x60, 'Numpad1': 0x61, 'Numpad2': 0x62, 'Numpad3': 0x63, 'Numpad4': 0x64, + 'Numpad5': 0x65, 'Numpad6': 0x66, 'Numpad7': 0x67, 'Numpad8': 0x68, 'Numpad9': 0x69, + 'NumpadMultiply': 0x6A, 'NumpadAdd': 0x6B, 'NumpadSeparator': 0x6C, + 'NumpadSubtract': 0x6D, 'NumpadDecimal': 0x6E, 'NumpadDivide': 0x6F, + 'NumpadEnter': 0x0D, // Same as Enter but with ENHANCED_KEY flag + 'NumLock': 0x90, - // Navigation - 'ArrowUp': 0x26, 'ArrowDown': 0x28, 'ArrowLeft': 0x25, 'ArrowRight': 0x27, - 'Home': 0x24, 'End': 0x23, 'PageUp': 0x21, 'PageDown': 0x22, - 'Insert': 0x2D, 'Delete': 0x2E, + // Navigation + 'ArrowUp': 0x26, 'ArrowDown': 0x28, 'ArrowLeft': 0x25, 'ArrowRight': 0x27, + 'Home': 0x24, 'End': 0x23, 'PageUp': 0x21, 'PageDown': 0x22, + 'Insert': 0x2D, 'Delete': 0x2E, - // Modifiers - 'ShiftLeft': 0x10, 'ShiftRight': 0x10, - 'ControlLeft': 0x11, 'ControlRight': 0x11, - 'AltLeft': 0x12, 'AltRight': 0x12, - 'MetaLeft': 0x5B, 'MetaRight': 0x5C, - 'CapsLock': 0x14, 'ScrollLock': 0x91, + // Modifiers + 'ShiftLeft': 0x10, 'ShiftRight': 0x10, + 'ControlLeft': 0x11, 'ControlRight': 0x11, + 'AltLeft': 0x12, 'AltRight': 0x12, + 'MetaLeft': 0x5B, 'MetaRight': 0x5C, + 'CapsLock': 0x14, 'ScrollLock': 0x91, - // Special keys - 'Escape': 0x1B, 'Enter': 0x0D, 'Tab': 0x09, 'Space': 0x20, - 'Backspace': 0x08, 'Pause': 0x13, 'ContextMenu': 0x5D, 'PrintScreen': 0x2C, + // Special keys + 'Escape': 0x1B, 'Enter': 0x0D, 'Tab': 0x09, 'Space': 0x20, + 'Backspace': 0x08, 'Pause': 0x13, 'ContextMenu': 0x5D, 'PrintScreen': 0x2C, - // OEM keys (US keyboard layout) - 'Semicolon': 0xBA, // ;: - 'Equal': 0xBB, // =+ - 'Comma': 0xBC, // ,< - 'Minus': 0xBD, // -_ - 'Period': 0xBE, // .> - 'Slash': 0xBF, // /? - 'Backquote': 0xC0, // `~ - 'BracketLeft': 0xDB, // [{ - 'Backslash': 0xDC, // \| - 'BracketRight': 0xDD, // ]} - 'Quote': 0xDE, // '" - 'IntlBackslash': 0xE2, // Non-US backslash -}; - -/** - * Mapping from browser KeyboardEvent.code to approximate Win32 scan codes. - * Note: Scan codes can vary by keyboard layout. These are approximations - * based on standard US keyboard layout. - */ -const CODE_TO_SCANCODE: { [code: string]: number } = { - // Letters (row by row) - 'KeyQ': 0x10, 'KeyW': 0x11, 'KeyE': 0x12, 'KeyR': 0x13, 'KeyT': 0x14, - 'KeyY': 0x15, 'KeyU': 0x16, 'KeyI': 0x17, 'KeyO': 0x18, 'KeyP': 0x19, - 'KeyA': 0x1E, 'KeyS': 0x1F, 'KeyD': 0x20, 'KeyF': 0x21, 'KeyG': 0x22, - 'KeyH': 0x23, 'KeyJ': 0x24, 'KeyK': 0x25, 'KeyL': 0x26, - 'KeyZ': 0x2C, 'KeyX': 0x2D, 'KeyC': 0x2E, 'KeyV': 0x2F, 'KeyB': 0x30, - 'KeyN': 0x31, 'KeyM': 0x32, - - // Digits - 'Digit1': 0x02, 'Digit2': 0x03, 'Digit3': 0x04, 'Digit4': 0x05, 'Digit5': 0x06, - 'Digit6': 0x07, 'Digit7': 0x08, 'Digit8': 0x09, 'Digit9': 0x0A, 'Digit0': 0x0B, - - // Function keys - 'F1': 0x3B, 'F2': 0x3C, 'F3': 0x3D, 'F4': 0x3E, 'F5': 0x3F, 'F6': 0x40, - 'F7': 0x41, 'F8': 0x42, 'F9': 0x43, 'F10': 0x44, 'F11': 0x57, 'F12': 0x58, - - // Numpad - 'Numpad0': 0x52, 'Numpad1': 0x4F, 'Numpad2': 0x50, 'Numpad3': 0x51, 'Numpad4': 0x4B, - 'Numpad5': 0x4C, 'Numpad6': 0x4D, 'Numpad7': 0x47, 'Numpad8': 0x48, 'Numpad9': 0x49, - 'NumpadMultiply': 0x37, 'NumpadAdd': 0x4E, 'NumpadSubtract': 0x4A, - 'NumpadDecimal': 0x53, 'NumpadDivide': 0x35, 'NumpadEnter': 0x1C, - 'NumLock': 0x45, - - // Navigation (extended keys) - 'ArrowUp': 0x48, 'ArrowDown': 0x50, 'ArrowLeft': 0x4B, 'ArrowRight': 0x4D, - 'Home': 0x47, 'End': 0x4F, 'PageUp': 0x49, 'PageDown': 0x51, - 'Insert': 0x52, 'Delete': 0x53, - - // Modifiers - 'ShiftLeft': 0x2A, 'ShiftRight': 0x36, - 'ControlLeft': 0x1D, 'ControlRight': 0x1D, - 'AltLeft': 0x38, 'AltRight': 0x38, - 'CapsLock': 0x3A, 'ScrollLock': 0x46, - - // Special keys - 'Escape': 0x01, 'Enter': 0x1C, 'Tab': 0x0F, 'Space': 0x39, - 'Backspace': 0x0E, 'Pause': 0x45, - - // OEM keys - 'Semicolon': 0x27, 'Equal': 0x0D, 'Comma': 0x33, 'Minus': 0x0C, - 'Period': 0x34, 'Slash': 0x35, 'Backquote': 0x29, - 'BracketLeft': 0x1A, 'Backslash': 0x2B, 'BracketRight': 0x1B, 'Quote': 0x28, -}; - -/** - * Codes that represent enhanced keys (extended keyboard keys). - */ -const ENHANCED_KEY_CODES = new Set([ - 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', - 'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', - 'NumpadEnter', 'NumpadDivide', - 'ControlRight', 'AltRight', - 'PrintScreen', 'Pause', 'ContextMenu', - 'MetaLeft', 'MetaRight', -]); - -/** - * Mapping of special keys (ev.key values) to their Unicode control character codes. - * These keys have multi-character ev.key strings but produce control characters. - * @see https://docs.microsoft.com/en-us/windows/console/key-event-record-str - */ -const KEY_TO_CONTROL_CHAR: { [key: string]: number } = { - 'Enter': 0x0D, // Carriage return - 'Backspace': 0x08, // Backspace - 'Tab': 0x09, // Horizontal tab - 'Escape': 0x1B, // Escape -}; - -/** - * Get the Win32 virtual key code for a keyboard event. - */ -function getVirtualKeyCode(ev: IKeyboardEvent): number { - // Try code-based lookup first - const vk = CODE_TO_VK[ev.code]; - if (vk !== undefined) { - return vk; - } - - // Fall back to keyCode for unmapped keys - // Note: keyCode is deprecated but provides reasonable fallback - return ev.keyCode || 0; -} - -/** - * Get the Win32 scan code for a keyboard event. - * Returns 0 if unknown (scan codes vary by hardware). - */ -function getScanCode(ev: IKeyboardEvent): number { - return CODE_TO_SCANCODE[ev.code] || 0; -} - -/** - * Get the unicode character for a keyboard event. - * Returns 0 for non-character keys. - */ -function getUnicodeChar(ev: IKeyboardEvent): number { - // Handle special keys that produce control characters - // Ctrl modifies some of these: Ctrl+Enter=LF, Ctrl+Backspace=DEL - if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { - if (ev.key === 'Enter') { - return 0x0A; // Line feed (Ctrl+Enter) - } - if (ev.key === 'Backspace') { - return 0x7F; // DEL (Ctrl+Backspace) - } - } - - // Check for special keys that always produce control characters - const controlChar = KEY_TO_CONTROL_CHAR[ev.key]; - if (controlChar !== undefined) { - return controlChar; - } - - // Only single-character keys produce unicode output - if (ev.key.length === 1) { - const codePoint = ev.key.codePointAt(0) || 0; - - // Handle Ctrl+letter combinations - these produce control characters (0x01-0x1A) - if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { - // Convert A-Z or a-z to control character (Ctrl+A = 0x01, Ctrl+C = 0x03, etc.) - if (codePoint >= 0x41 && codePoint <= 0x5A) { // A-Z - return codePoint - 0x40; - } - if (codePoint >= 0x61 && codePoint <= 0x7A) { // a-z - return codePoint - 0x60; - } - } - - return codePoint; - } - return 0; -} - -/** - * Get the Win32 control key state flags. - */ -function getControlKeyState(ev: IKeyboardEvent): number { - let state = 0; - - if (ev.shiftKey) { - state |= Win32ControlKeyState.SHIFT_PRESSED; - } - - // Note: We can't distinguish left/right for ctrl/alt in standard browser events, - // so we use the generic pressed flags. The right-side flags are used when - // we can detect them (e.g., via code property). - if (ev.ctrlKey) { - if (ev.code === 'ControlRight') { - state |= Win32ControlKeyState.RIGHT_CTRL_PRESSED; - } else { - state |= Win32ControlKeyState.LEFT_CTRL_PRESSED; - } - } - - if (ev.altKey) { - if (ev.code === 'AltRight') { - state |= Win32ControlKeyState.RIGHT_ALT_PRESSED; - } else { - state |= Win32ControlKeyState.LEFT_ALT_PRESSED; - } - } - - // Check for enhanced key - if (ENHANCED_KEY_CODES.has(ev.code)) { - state |= Win32ControlKeyState.ENHANCED_KEY; - } - - // Note: CapsLock, NumLock, ScrollLock states are not reliably available - // in standard browser keyboard events. We could potentially detect them - // via getModifierState() but this may not be available in all environments. - - return state; -} - -/** - * Evaluate a keyboard event using Win32 input mode. - * - * @param ev The keyboard event. - * @param isKeyDown Whether this is a keydown (true) or keyup (false) event. - * @returns The keyboard result with the encoded key sequence. - */ -export function evaluateKeyboardEventWin32( - ev: IKeyboardEvent, - isKeyDown: boolean -): IKeyboardResult { - const result: IKeyboardResult = { - type: KeyboardResultType.SEND_KEY, - cancel: false, - key: undefined + // OEM keys (US keyboard layout) + 'Semicolon': 0xBA, // ;: + 'Equal': 0xBB, // =+ + 'Comma': 0xBC, // ,< + 'Minus': 0xBD, // -_ + 'Period': 0xBE, // .> + 'Slash': 0xBF, // /? + 'Backquote': 0xC0, // `~ + 'BracketLeft': 0xDB, // [{ + 'Backslash': 0xDC, // \| + 'BracketRight': 0xDD, // ]} + 'Quote': 0xDE, // '" + 'IntlBackslash': 0xE2 // Non-US backslash }; - const vk = getVirtualKeyCode(ev); - const sc = getScanCode(ev); - const uc = getUnicodeChar(ev); - const kd = isKeyDown ? 1 : 0; - const cs = getControlKeyState(ev); - const rc = 1; // Repeat count, always 1 for now + /** + * Mapping from browser KeyboardEvent.code to approximate Win32 scan codes. + * Note: Scan codes can vary by keyboard layout. These are approximations + * based on standard US keyboard layout. + */ + private readonly _codeToScancode: { [code: string]: number } = { + // Letters (row by row) + 'KeyQ': 0x10, 'KeyW': 0x11, 'KeyE': 0x12, 'KeyR': 0x13, 'KeyT': 0x14, + 'KeyY': 0x15, 'KeyU': 0x16, 'KeyI': 0x17, 'KeyO': 0x18, 'KeyP': 0x19, + 'KeyA': 0x1E, 'KeyS': 0x1F, 'KeyD': 0x20, 'KeyF': 0x21, 'KeyG': 0x22, + 'KeyH': 0x23, 'KeyJ': 0x24, 'KeyK': 0x25, 'KeyL': 0x26, + 'KeyZ': 0x2C, 'KeyX': 0x2D, 'KeyC': 0x2E, 'KeyV': 0x2F, 'KeyB': 0x30, + 'KeyN': 0x31, 'KeyM': 0x32, - // Format: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ - result.key = `${C0.ESC}[${vk};${sc};${uc};${kd};${cs};${rc}_`; - result.cancel = true; + // Digits + 'Digit1': 0x02, 'Digit2': 0x03, 'Digit3': 0x04, 'Digit4': 0x05, 'Digit5': 0x06, + 'Digit6': 0x07, 'Digit7': 0x08, 'Digit8': 0x09, 'Digit9': 0x0A, 'Digit0': 0x0B, - return result; + // Function keys + 'F1': 0x3B, 'F2': 0x3C, 'F3': 0x3D, 'F4': 0x3E, 'F5': 0x3F, 'F6': 0x40, + 'F7': 0x41, 'F8': 0x42, 'F9': 0x43, 'F10': 0x44, 'F11': 0x57, 'F12': 0x58, + + // Numpad + 'Numpad0': 0x52, 'Numpad1': 0x4F, 'Numpad2': 0x50, 'Numpad3': 0x51, 'Numpad4': 0x4B, + 'Numpad5': 0x4C, 'Numpad6': 0x4D, 'Numpad7': 0x47, 'Numpad8': 0x48, 'Numpad9': 0x49, + 'NumpadMultiply': 0x37, 'NumpadAdd': 0x4E, 'NumpadSubtract': 0x4A, + 'NumpadDecimal': 0x53, 'NumpadDivide': 0x35, 'NumpadEnter': 0x1C, + 'NumLock': 0x45, + + // Navigation (extended keys) + 'ArrowUp': 0x48, 'ArrowDown': 0x50, 'ArrowLeft': 0x4B, 'ArrowRight': 0x4D, + 'Home': 0x47, 'End': 0x4F, 'PageUp': 0x49, 'PageDown': 0x51, + 'Insert': 0x52, 'Delete': 0x53, + + // Modifiers + 'ShiftLeft': 0x2A, 'ShiftRight': 0x36, + 'ControlLeft': 0x1D, 'ControlRight': 0x1D, + 'AltLeft': 0x38, 'AltRight': 0x38, + 'CapsLock': 0x3A, 'ScrollLock': 0x46, + + // Special keys + 'Escape': 0x01, 'Enter': 0x1C, 'Tab': 0x0F, 'Space': 0x39, + 'Backspace': 0x0E, 'Pause': 0x45, + + // OEM keys + 'Semicolon': 0x27, 'Equal': 0x0D, 'Comma': 0x33, 'Minus': 0x0C, + 'Period': 0x34, 'Slash': 0x35, 'Backquote': 0x29, + 'BracketLeft': 0x1A, 'Backslash': 0x2B, 'BracketRight': 0x1B, 'Quote': 0x28 + }; + + /** + * Codes that represent enhanced keys (extended keyboard keys). + */ + private readonly _enhancedKeyCodes = new Set([ + 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', + 'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', + 'NumpadEnter', 'NumpadDivide', + 'ControlRight', 'AltRight', + 'PrintScreen', 'Pause', 'ContextMenu', + 'MetaLeft', 'MetaRight' + ]); + + /** + * Mapping of special keys (ev.key values) to their Unicode control character codes. + * These keys have multi-character ev.key strings but produce control characters. + * @see https://docs.microsoft.com/en-us/windows/console/key-event-record-str + */ + private readonly _keyToControlChar: { [key: string]: number } = { + 'Enter': 0x0D, // Carriage return + 'Backspace': 0x08, // Backspace + 'Tab': 0x09, // Horizontal tab + 'Escape': 0x1B // Escape + }; + + /** + * Get the Win32 virtual key code for a keyboard event. + */ + private _getVirtualKeyCode(ev: IKeyboardEvent): number { + const vk = this._codeToVk[ev.code]; + if (vk !== undefined) { + return vk; + } + // Fall back to keyCode for unmapped keys + return ev.keyCode || 0; + } + + /** + * Get the Win32 scan code for a keyboard event. + * Returns 0 if unknown (scan codes vary by hardware). + */ + private _getScanCode(ev: IKeyboardEvent): number { + return this._codeToScancode[ev.code] || 0; + } + + /** + * Get the unicode character for a keyboard event. + * Returns 0 for non-character keys. + */ + private _getUnicodeChar(ev: IKeyboardEvent): number { + // Handle special keys that produce control characters + // Ctrl modifies some of these: Ctrl+Enter=LF, Ctrl+Backspace=DEL + if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { + if (ev.key === 'Enter') { + return 0x0A; // Line feed (Ctrl+Enter) + } + if (ev.key === 'Backspace') { + return 0x7F; // DEL (Ctrl+Backspace) + } + } + + // Check for special keys that always produce control characters + const controlChar = this._keyToControlChar[ev.key]; + if (controlChar !== undefined) { + return controlChar; + } + + // Only single-character keys produce unicode output + if (ev.key.length === 1) { + const codePoint = ev.key.codePointAt(0) || 0; + + // Handle Ctrl+letter combinations - these produce control characters (0x01-0x1A) + if (ev.ctrlKey && !ev.altKey && !ev.metaKey) { + // Convert A-Z or a-z to control character (Ctrl+A = 0x01, Ctrl+C = 0x03, etc.) + if (codePoint >= 0x41 && codePoint <= 0x5A) { // A-Z + return codePoint - 0x40; + } + if (codePoint >= 0x61 && codePoint <= 0x7A) { // a-z + return codePoint - 0x60; + } + } + + return codePoint; + } + return 0; + } + + /** + * Get the Win32 control key state flags. + */ + private _getControlKeyState(ev: IKeyboardEvent): number { + let state = 0; + + if (ev.shiftKey) { + state |= Win32ControlKeyState.SHIFT_PRESSED; + } + + // Note: We can't distinguish left/right for ctrl/alt in standard browser events, + // so we use the generic pressed flags. The right-side flags are used when + // we can detect them (e.g., via code property). + if (ev.ctrlKey) { + if (ev.code === 'ControlRight') { + state |= Win32ControlKeyState.RIGHT_CTRL_PRESSED; + } else { + state |= Win32ControlKeyState.LEFT_CTRL_PRESSED; + } + } + + if (ev.altKey) { + if (ev.code === 'AltRight') { + state |= Win32ControlKeyState.RIGHT_ALT_PRESSED; + } else { + state |= Win32ControlKeyState.LEFT_ALT_PRESSED; + } + } + + // Check for enhanced key + if (this._enhancedKeyCodes.has(ev.code)) { + state |= Win32ControlKeyState.ENHANCED_KEY; + } + + return state; + } + + /** + * Evaluate a keyboard event using Win32 input mode. + * + * @param ev The keyboard event. + * @param isKeyDown Whether this is a keydown (true) or keyup (false) event. + * @returns The keyboard result with the encoded key sequence. + */ + public evaluateKeyboardEvent(ev: IKeyboardEvent, isKeyDown: boolean): IKeyboardResult { + const vk = this._getVirtualKeyCode(ev); + const sc = this._getScanCode(ev); + const uc = this._getUnicodeChar(ev); + const kd = isKeyDown ? 1 : 0; + const cs = this._getControlKeyState(ev); + const rc = 1; // Repeat count, always 1 for now + + // Format: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + return { + type: KeyboardResultType.SEND_KEY, + cancel: true, + key: `${C0.ESC}[${vk};${sc};${uc};${kd};${cs};${rc}_` + }; + } } From de01d4608cf4a1e3d2b75b8e28e7f87bd346754c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:09:48 -0800 Subject: [PATCH 26/31] Move kitty input mode into a class to avoid init cost --- src/browser/services/KeyboardService.ts | 16 +- src/common/input/KittyKeyboard.test.ts | 260 ++++---- src/common/input/KittyKeyboard.ts | 839 ++++++++++++------------ 3 files changed, 551 insertions(+), 564 deletions(-) diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index afdde381..d9e6c1ee 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -5,7 +5,7 @@ import { IKeyboardService } from 'browser/services/Services'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; -import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard'; +import { KittyKeyboard, KittyKeyboardEventType, KittyKeyboardFlags } from 'common/input/KittyKeyboard'; import { Win32InputMode } from 'common/input/Win32InputMode'; import { isMac } from 'common/Platform'; import { ICoreService, IOptionsService } from 'common/services/Services'; @@ -15,6 +15,7 @@ export class KeyboardService implements IKeyboardService { public serviceBrand: undefined; private _win32InputMode: Win32InputMode | undefined; + private _kittyKeyboard: KittyKeyboard | undefined; constructor( @ICoreService private readonly _coreService: ICoreService, @@ -29,6 +30,13 @@ export class KeyboardService implements IKeyboardService { return this._win32InputMode; } + private _getKittyKeyboard(): KittyKeyboard { + if (!this._kittyKeyboard) { + this._kittyKeyboard = new KittyKeyboard(); + } + return this._kittyKeyboard; + } + public evaluateKeyDown(event: KeyboardEvent): IKeyboardResult { // Win32 input mode takes priority (most raw) if (this.useWin32InputMode) { @@ -36,7 +44,7 @@ export class KeyboardService implements IKeyboardService { } const kittyFlags = this._coreService.kittyKeyboard.flags; return this.useKitty - ? evaluateKeyboardEventKitty(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS) + ? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS) : evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, isMac, this._optionsService.rawOptions.macOptionIsMeta); } @@ -47,14 +55,14 @@ export class KeyboardService implements IKeyboardService { } const kittyFlags = this._coreService.kittyKeyboard.flags; if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) { - return evaluateKeyboardEventKitty(event, kittyFlags, KittyKeyboardEventType.RELEASE); + return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE); } return undefined; } public get useKitty(): boolean { const kittyFlags = this._coreService.kittyKeyboard.flags; - return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && shouldUseKittyProtocol(kittyFlags)); + return !!(this._optionsService.rawOptions.vtExtensions?.kittyKeyboard && KittyKeyboard.shouldUseProtocol(kittyFlags)); } public get useWin32InputMode(): boolean { diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts index e346708f..b9b59baa 100644 --- a/src/common/input/KittyKeyboard.test.ts +++ b/src/common/input/KittyKeyboard.test.ts @@ -1,7 +1,7 @@ import { assert } from 'chai'; -import { evaluateKeyboardEventKitty, KittyKeyboardEventType, KittyKeyboardFlags, shouldUseKittyProtocol } from 'common/input/KittyKeyboard'; -import { IKeyboardResult, IKeyboardEvent } from 'common/Types'; +import { KittyKeyboard, KittyKeyboardEventType, KittyKeyboardFlags } from 'common/input/KittyKeyboard'; +import { IKeyboardEvent } from 'common/Types'; function createEvent(partialEvent: Partial = {}): IKeyboardEvent { return { @@ -17,71 +17,77 @@ function createEvent(partialEvent: Partial = {}): IKeyboardEvent } describe('KittyKeyboard', () => { - describe('shouldUseKittyProtocol', () => { + let kitty: KittyKeyboard; + + beforeEach(() => { + kitty = new KittyKeyboard(); + }); + + describe('shouldUseProtocol', () => { it('should return false when flags are 0', () => { - assert.strictEqual(shouldUseKittyProtocol(0), false); + assert.strictEqual(KittyKeyboard.shouldUseProtocol(0), false); }); it('should return true when any flag is set', () => { - assert.strictEqual(shouldUseKittyProtocol(KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES), true); - assert.strictEqual(shouldUseKittyProtocol(KittyKeyboardFlags.REPORT_EVENT_TYPES), true); - assert.strictEqual(shouldUseKittyProtocol(0b11111), true); + assert.strictEqual(KittyKeyboard.shouldUseProtocol(KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES), true); + assert.strictEqual(KittyKeyboard.shouldUseProtocol(KittyKeyboardFlags.REPORT_EVENT_TYPES), true); + assert.strictEqual(KittyKeyboard.shouldUseProtocol(0b11111), true); }); }); - describe('evaluateKeyboardEventKitty', () => { + describe('evaluate', () => { describe('modifier encoding (value = 1 + modifiers)', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('shift+letter sends plain character in DISAMBIGUATE mode', () => { // Kitty spec: DISAMBIGUATE only encodes keys ambiguous in legacy encoding // Shift+a → "A" is not ambiguous, so send plain "A" - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags); assert.strictEqual(result.key, 'A'); }); it('alt=3 (1+2) still uses CSI u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', altKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', altKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;3u'); }); it('ctrl=5 (1+4)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;5u'); }); it('super/meta=9 (1+8)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', metaKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', metaKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;9u'); }); it('ctrl+shift=6 (1+4+1)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;6u'); }); it('ctrl+alt=7 (1+4+2)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, altKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, altKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;7u'); }); it('ctrl+alt+shift=8 (1+4+2+1)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, altKey: true, shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, altKey: true, shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;8u'); }); it('ctrl+super=13 (1+4+8)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true, metaKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true, metaKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;13u'); }); it('all four modifiers=16 (1+1+2+4+8)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true, altKey: true, ctrlKey: true, metaKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', shiftKey: true, altKey: true, ctrlKey: true, metaKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;16u'); }); it('no modifiers omits modifier field', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags); assert.strictEqual(result.key, '\x1b[27u'); }); }); @@ -90,42 +96,42 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('Escape → CSI 27 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags); assert.strictEqual(result.key, '\x1b[27u'); }); it('Enter → CSI 13 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags); assert.strictEqual(result.key, '\x1b[13u'); }); it('Tab → CSI 9 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags); assert.strictEqual(result.key, '\x1b[9u'); }); it('Backspace → CSI 127 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Backspace' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags); assert.strictEqual(result.key, '\x1b[127u'); }); it('Space → CSI 32 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags); + const result = kitty.evaluate(createEvent({ key: ' ' }), flags); assert.strictEqual(result.key, '\x1b[32u'); }); it('Shift+Tab → CSI 9;2 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Tab', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Tab', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[9;2u'); }); it('Ctrl+Enter → CSI 13;5 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Enter', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[13;5u'); }); it('Alt+Escape → CSI 27;3 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape', altKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Escape', altKey: true }), flags); assert.strictEqual(result.key, '\x1b[27;3u'); }); }); @@ -134,42 +140,42 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('Insert → CSI 2 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Insert' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Insert' }), flags); assert.strictEqual(result.key, '\x1b[2~'); }); it('Delete → CSI 3 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Delete' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Delete' }), flags); assert.strictEqual(result.key, '\x1b[3~'); }); it('PageUp → CSI 5 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageUp' }), flags); + const result = kitty.evaluate(createEvent({ key: 'PageUp' }), flags); assert.strictEqual(result.key, '\x1b[5~'); }); it('PageDown → CSI 6 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageDown' }), flags); + const result = kitty.evaluate(createEvent({ key: 'PageDown' }), flags); assert.strictEqual(result.key, '\x1b[6~'); }); it('Home → CSI H', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Home' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Home' }), flags); assert.strictEqual(result.key, '\x1b[H'); }); it('End → CSI F', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'End' }), flags); + const result = kitty.evaluate(createEvent({ key: 'End' }), flags); assert.strictEqual(result.key, '\x1b[F'); }); it('Shift+PageUp → CSI 5;2 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'PageUp', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'PageUp', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[5;2~'); }); it('Ctrl+Home → CSI 1;5 H', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Home', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Home', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[1;5H'); }); }); @@ -178,37 +184,37 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('ArrowUp → CSI A', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp' }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowUp' }), flags); assert.strictEqual(result.key, '\x1b[A'); }); it('ArrowDown → CSI B', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowDown' }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowDown' }), flags); assert.strictEqual(result.key, '\x1b[B'); }); it('ArrowRight → CSI C', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight' }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowRight' }), flags); assert.strictEqual(result.key, '\x1b[C'); }); it('ArrowLeft → CSI D', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft' }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowLeft' }), flags); assert.strictEqual(result.key, '\x1b[D'); }); it('Shift+ArrowUp → CSI 1;2 A', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowUp', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowUp', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[1;2A'); }); it('Ctrl+ArrowLeft → CSI 1;5 D', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowLeft', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowLeft', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[1;5D'); }); it('Ctrl+Shift+ArrowRight → CSI 1;6 C', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ArrowRight', ctrlKey: true, shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'ArrowRight', ctrlKey: true, shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[1;6C'); }); }); @@ -217,72 +223,72 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('F1 → CSI P (SS3 form)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F1' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F1' }), flags); assert.strictEqual(result.key, '\x1bOP'); }); it('F2 → CSI Q (SS3 form)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F2' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F2' }), flags); assert.strictEqual(result.key, '\x1bOQ'); }); it('F3 → CSI R (SS3 form)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F3' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F3' }), flags); assert.strictEqual(result.key, '\x1bOR'); }); it('F4 → CSI S (SS3 form)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F4' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F4' }), flags); assert.strictEqual(result.key, '\x1bOS'); }); it('F5 → CSI 15 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F5' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F5' }), flags); assert.strictEqual(result.key, '\x1b[15~'); }); it('F6 → CSI 17 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F6' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F6' }), flags); assert.strictEqual(result.key, '\x1b[17~'); }); it('F7 → CSI 18 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F7' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F7' }), flags); assert.strictEqual(result.key, '\x1b[18~'); }); it('F8 → CSI 19 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F8' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F8' }), flags); assert.strictEqual(result.key, '\x1b[19~'); }); it('F9 → CSI 20 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F9' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F9' }), flags); assert.strictEqual(result.key, '\x1b[20~'); }); it('F10 → CSI 21 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F10' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F10' }), flags); assert.strictEqual(result.key, '\x1b[21~'); }); it('F11 → CSI 23 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F11' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F11' }), flags); assert.strictEqual(result.key, '\x1b[23~'); }); it('F12 → CSI 24 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F12' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F12' }), flags); assert.strictEqual(result.key, '\x1b[24~'); }); it('Shift+F1 → CSI 1;2 P', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F1', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'F1', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[1;2P'); }); it('Ctrl+F5 → CSI 15;5 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F5', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'F5', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[15;5~'); }); }); @@ -291,22 +297,22 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('F13 → CSI 57376 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F13' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F13' }), flags); assert.strictEqual(result.key, '\x1b[57376u'); }); it('F14 → CSI 57377 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F14' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F14' }), flags); assert.strictEqual(result.key, '\x1b[57377u'); }); it('F20 → CSI 57383 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F20' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F20' }), flags); assert.strictEqual(result.key, '\x1b[57383u'); }); it('F24 → CSI 57387 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'F24' }), flags); + const result = kitty.evaluate(createEvent({ key: 'F24' }), flags); assert.strictEqual(result.key, '\x1b[57387u'); }); }); @@ -315,57 +321,57 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('Numpad0 → CSI 57399 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '0', code: 'Numpad0' }), flags); + const result = kitty.evaluate(createEvent({ key: '0', code: 'Numpad0' }), flags); assert.strictEqual(result.key, '\x1b[57399u'); }); it('Numpad1 → CSI 57400 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '1', code: 'Numpad1' }), flags); + const result = kitty.evaluate(createEvent({ key: '1', code: 'Numpad1' }), flags); assert.strictEqual(result.key, '\x1b[57400u'); }); it('Numpad9 → CSI 57408 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '9', code: 'Numpad9' }), flags); + const result = kitty.evaluate(createEvent({ key: '9', code: 'Numpad9' }), flags); assert.strictEqual(result.key, '\x1b[57408u'); }); it('NumpadDecimal → CSI 57409 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '.', code: 'NumpadDecimal' }), flags); + const result = kitty.evaluate(createEvent({ key: '.', code: 'NumpadDecimal' }), flags); assert.strictEqual(result.key, '\x1b[57409u'); }); it('NumpadDivide → CSI 57410 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '/', code: 'NumpadDivide' }), flags); + const result = kitty.evaluate(createEvent({ key: '/', code: 'NumpadDivide' }), flags); assert.strictEqual(result.key, '\x1b[57410u'); }); it('NumpadMultiply → CSI 57411 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '*', code: 'NumpadMultiply' }), flags); + const result = kitty.evaluate(createEvent({ key: '*', code: 'NumpadMultiply' }), flags); assert.strictEqual(result.key, '\x1b[57411u'); }); it('NumpadSubtract → CSI 57412 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '-', code: 'NumpadSubtract' }), flags); + const result = kitty.evaluate(createEvent({ key: '-', code: 'NumpadSubtract' }), flags); assert.strictEqual(result.key, '\x1b[57412u'); }); it('NumpadAdd → CSI 57413 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '+', code: 'NumpadAdd' }), flags); + const result = kitty.evaluate(createEvent({ key: '+', code: 'NumpadAdd' }), flags); assert.strictEqual(result.key, '\x1b[57413u'); }); it('NumpadEnter → CSI 57414 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Enter', code: 'NumpadEnter' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Enter', code: 'NumpadEnter' }), flags); assert.strictEqual(result.key, '\x1b[57414u'); }); it('NumpadEqual → CSI 57415 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '=', code: 'NumpadEqual' }), flags); + const result = kitty.evaluate(createEvent({ key: '=', code: 'NumpadEqual' }), flags); assert.strictEqual(result.key, '\x1b[57415u'); }); it('Ctrl+Numpad5 → CSI 57404;5 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '5', code: 'Numpad5', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: '5', code: 'Numpad5', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[57404;5u'); }); }); @@ -374,57 +380,57 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES; it('Left Shift → CSI 57441 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[57441;2u'); }); it('Right Shift → CSI 57447 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftRight', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftRight', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[57447;2u'); }); it('Left Control → CSI 57442 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlLeft', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Control', code: 'ControlLeft', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[57442;5u'); }); it('Right Control → CSI 57448 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Control', code: 'ControlRight', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[57448;5u'); }); it('Left Alt → CSI 57443 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Alt', code: 'AltLeft', altKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Alt', code: 'AltLeft', altKey: true }), flags); assert.strictEqual(result.key, '\x1b[57443;3u'); }); it('Right Alt → CSI 57449 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Alt', code: 'AltRight', altKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Alt', code: 'AltRight', altKey: true }), flags); assert.strictEqual(result.key, '\x1b[57449;3u'); }); it('Left Meta/Super → CSI 57444 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Meta', code: 'MetaLeft', metaKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Meta', code: 'MetaLeft', metaKey: true }), flags); assert.strictEqual(result.key, '\x1b[57444;9u'); }); it('Right Meta/Super → CSI 57450 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Meta', code: 'MetaRight', metaKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Meta', code: 'MetaRight', metaKey: true }), flags); assert.strictEqual(result.key, '\x1b[57450;9u'); }); it('CapsLock → CSI 57358 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'CapsLock', code: 'CapsLock' }), flags); + const result = kitty.evaluate(createEvent({ key: 'CapsLock', code: 'CapsLock' }), flags); assert.strictEqual(result.key, '\x1b[57358u'); }); it('NumLock → CSI 57360 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'NumLock', code: 'NumLock' }), flags); + const result = kitty.evaluate(createEvent({ key: 'NumLock', code: 'NumLock' }), flags); assert.strictEqual(result.key, '\x1b[57360u'); }); it('ScrollLock → CSI 57359 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ScrollLock', code: 'ScrollLock' }), flags); + const result = kitty.evaluate(createEvent({ key: 'ScrollLock', code: 'ScrollLock' }), flags); assert.strictEqual(result.key, '\x1b[57359u'); }); }); @@ -433,42 +439,42 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES; it('press event (default, no suffix)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS); + const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS); assert.strictEqual(result.key, '\x1b[97u'); }); it('press event explicit :1 when modifiers present', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS); assert.strictEqual(result.key, '\x1b[97;5u'); }); it('repeat event → :2 suffix', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT); + const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT); assert.strictEqual(result.key, '\x1b[97;1:2u'); }); it('release event → :3 suffix', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, '\x1b[97;1:3u'); }); it('release with modifier → mod:3', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, '\x1b[97;5:3u'); }); it('repeat with modifier → mod:2', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', shiftKey: true, altKey: true }), flags, KittyKeyboardEventType.REPEAT); + const result = kitty.evaluate(createEvent({ key: 'a', shiftKey: true, altKey: true }), flags, KittyKeyboardEventType.REPEAT); assert.strictEqual(result.key, '\x1b[97;4:2u'); }); it('functional key release → CSI code;1:3 ~', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Delete' }), flags, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'Delete' }), flags, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, '\x1b[3;1:3~'); }); it('modifier key release includes its own bit cleared', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'Shift', code: 'ShiftLeft', shiftKey: false }), flags, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, '\x1b[57441;1:3u'); }); }); @@ -477,34 +483,34 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES; it('lowercase letter → CSI codepoint u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags); + const result = kitty.evaluate(createEvent({ key: 'a' }), flags); assert.strictEqual(result.key, '\x1b[97u'); }); it('uppercase letter uses lowercase codepoint', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;2u'); }); it('digit → CSI codepoint u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '5' }), flags); + const result = kitty.evaluate(createEvent({ key: '5' }), flags); assert.strictEqual(result.key, '\x1b[53u'); }); it('punctuation → CSI codepoint u', () => { - assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '.' }), flags).key, '\x1b[46u'); - assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ',' }), flags).key, '\x1b[44u'); - assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ';' }), flags).key, '\x1b[59u'); - assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '/' }), flags).key, '\x1b[47u'); + assert.strictEqual(kitty.evaluate(createEvent({ key: '.' }), flags).key, '\x1b[46u'); + assert.strictEqual(kitty.evaluate(createEvent({ key: ',' }), flags).key, '\x1b[44u'); + assert.strictEqual(kitty.evaluate(createEvent({ key: ';' }), flags).key, '\x1b[59u'); + assert.strictEqual(kitty.evaluate(createEvent({ key: '/' }), flags).key, '\x1b[47u'); }); it('brackets → CSI codepoint u', () => { - assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: '[' }), flags).key, '\x1b[91u'); - assert.strictEqual(evaluateKeyboardEventKitty(createEvent({ key: ']' }), flags).key, '\x1b[93u'); + assert.strictEqual(kitty.evaluate(createEvent({ key: '[' }), flags).key, '\x1b[91u'); + assert.strictEqual(kitty.evaluate(createEvent({ key: ']' }), flags).key, '\x1b[93u'); }); it('space → CSI 32 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: ' ' }), flags); + const result = kitty.evaluate(createEvent({ key: ' ' }), flags); assert.strictEqual(result.key, '\x1b[32u'); }); }); @@ -513,38 +519,38 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT; it('regular key includes text codepoint', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags); + const result = kitty.evaluate(createEvent({ key: 'a' }), flags); assert.strictEqual(result.key, '\x1b[97;;97u'); }); it('shifted key includes shifted text', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;2;65u'); }); it('Ctrl+key omits text (control code)', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', ctrlKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;5u'); }); it('functional key has no text', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags); assert.strictEqual(result.key, '\x1b[27u'); }); it('release event has no text', () => { const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES; - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'a' }), flagsWithEvents, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, '\x1b[97;1:3u'); }); it('digit with text', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '5' }), flags); + const result = kitty.evaluate(createEvent({ key: '5' }), flags); assert.strictEqual(result.key, '\x1b[53;;53u'); }); it('Shift+digit shows shifted symbol', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags); + const result = kitty.evaluate(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags); assert.strictEqual(result.key, '\x1b[53;2;37u'); }); }); @@ -553,22 +559,22 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ALTERNATE_KEYS; it('Shift+a includes shifted key → CSI 97:65 ; 2 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags); assert.strictEqual(result.key, '\x1b[97:65;2u'); }); it('unshifted key has no alternate', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a', code: 'KeyA' }), flags); + const result = kitty.evaluate(createEvent({ key: 'a', code: 'KeyA' }), flags); assert.strictEqual(result.key, '\x1b[97u'); }); it('Shift+5 includes shifted key → CSI 53:37 ; 2 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags); + const result = kitty.evaluate(createEvent({ key: '%', shiftKey: true, code: 'Digit5' }), flags); assert.strictEqual(result.key, '\x1b[53:37;2u'); }); it('functional keys have no shifted alternate', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Escape', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'Escape', shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[27;2u'); }); }); @@ -577,13 +583,13 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ALTERNATE_KEYS | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT; it('Shift+a → CSI 97:65 ; 2 ; 65 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flags); assert.strictEqual(result.key, '\x1b[97:65;2;65u'); }); it('Shift+a release → CSI 97:65 ; 2:3 u (no text)', () => { const flagsWithEvents = flags | KittyKeyboardFlags.REPORT_EVENT_TYPES; - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flagsWithEvents, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true, code: 'KeyA' }), flagsWithEvents, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, '\x1b[97:65;2:3u'); }); }); @@ -592,7 +598,7 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('should not generate key sequence for release events', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE); + const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.RELEASE); assert.strictEqual(result.key, undefined); }); }); @@ -602,37 +608,37 @@ describe('KittyKeyboard', () => { it('shift+letter sends plain character in DISAMBIGUATE mode', () => { // Shift+A produces printable "A", not ambiguous, so send plain character - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', shiftKey: true }), flags); assert.strictEqual(result.key, 'A'); }); it('ctrl+shift+a sends lowercase codepoint 97', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'A', ctrlKey: true, shiftKey: true }), flags); + const result = kitty.evaluate(createEvent({ key: 'A', ctrlKey: true, shiftKey: true }), flags); assert.strictEqual(result.key, '\x1b[97;6u'); }); it('Dead key produces no output', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Dead' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Dead' }), flags); assert.strictEqual(result.key, undefined); }); it('Unidentified key produces no output', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Unidentified' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Unidentified' }), flags); assert.strictEqual(result.key, undefined); }); it('PrintScreen → CSI 57361 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'PrintScreen' }), flags); + const result = kitty.evaluate(createEvent({ key: 'PrintScreen' }), flags); assert.strictEqual(result.key, '\x1b[57361u'); }); it('Pause → CSI 57362 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'Pause' }), flags); + const result = kitty.evaluate(createEvent({ key: 'Pause' }), flags); assert.strictEqual(result.key, '\x1b[57362u'); }); it('ContextMenu → CSI 57363 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'ContextMenu' }), flags); + const result = kitty.evaluate(createEvent({ key: 'ContextMenu' }), flags); assert.strictEqual(result.key, '\x1b[57363u'); }); }); @@ -641,37 +647,37 @@ describe('KittyKeyboard', () => { const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; it('MediaPlayPause → CSI 57430 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaPlayPause' }), flags); + const result = kitty.evaluate(createEvent({ key: 'MediaPlayPause' }), flags); assert.strictEqual(result.key, '\x1b[57430u'); }); it('MediaStop → CSI 57432 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaStop' }), flags); + const result = kitty.evaluate(createEvent({ key: 'MediaStop' }), flags); assert.strictEqual(result.key, '\x1b[57432u'); }); it('MediaTrackNext → CSI 57435 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaTrackNext' }), flags); + const result = kitty.evaluate(createEvent({ key: 'MediaTrackNext' }), flags); assert.strictEqual(result.key, '\x1b[57435u'); }); it('MediaTrackPrevious → CSI 57436 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'MediaTrackPrevious' }), flags); + const result = kitty.evaluate(createEvent({ key: 'MediaTrackPrevious' }), flags); assert.strictEqual(result.key, '\x1b[57436u'); }); it('AudioVolumeDown → CSI 57438 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeDown' }), flags); + const result = kitty.evaluate(createEvent({ key: 'AudioVolumeDown' }), flags); assert.strictEqual(result.key, '\x1b[57438u'); }); it('AudioVolumeUp → CSI 57439 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeUp' }), flags); + const result = kitty.evaluate(createEvent({ key: 'AudioVolumeUp' }), flags); assert.strictEqual(result.key, '\x1b[57439u'); }); it('AudioVolumeMute → CSI 57440 u', () => { - const result = evaluateKeyboardEventKitty(createEvent({ key: 'AudioVolumeMute' }), flags); + const result = kitty.evaluate(createEvent({ key: 'AudioVolumeMute' }), flags); assert.strictEqual(result.key, '\x1b[57440u'); }); }); diff --git a/src/common/input/KittyKeyboard.ts b/src/common/input/KittyKeyboard.ts index f2d7388b..c9db3dad 100644 --- a/src/common/input/KittyKeyboard.ts +++ b/src/common/input/KittyKeyboard.ts @@ -51,468 +51,441 @@ export const enum KittyKeyboardModifiers { } /** - * Functional key codes for Kitty protocol. - * Keys that don't produce text have specific unicode codepoint mappings. + * Kitty keyboard protocol handler class. + * Encapsulates all key code mappings and encoding logic. */ -const FUNCTIONAL_KEY_CODES: { [key: string]: number } = { - 'Escape': 27, - 'Enter': 13, - 'Tab': 9, - 'Backspace': 127, - 'CapsLock': 57358, - 'ScrollLock': 57359, - 'NumLock': 57360, - 'PrintScreen': 57361, - 'Pause': 57362, - 'ContextMenu': 57363, - // F13-F35 (F1-F12 use legacy encoding) - 'F13': 57376, - 'F14': 57377, - 'F15': 57378, - 'F16': 57379, - 'F17': 57380, - 'F18': 57381, - 'F19': 57382, - 'F20': 57383, - 'F21': 57384, - 'F22': 57385, - 'F23': 57386, - 'F24': 57387, - 'F25': 57388, - // Keypad keys - 'KP_0': 57399, - 'KP_1': 57400, - 'KP_2': 57401, - 'KP_3': 57402, - 'KP_4': 57403, - 'KP_5': 57404, - 'KP_6': 57405, - 'KP_7': 57406, - 'KP_8': 57407, - 'KP_9': 57408, - 'KP_Decimal': 57409, - 'KP_Divide': 57410, - 'KP_Multiply': 57411, - 'KP_Subtract': 57412, - 'KP_Add': 57413, - 'KP_Enter': 57414, - 'KP_Equal': 57415, - // Modifier keys - 'ShiftLeft': 57441, - 'ShiftRight': 57447, - 'ControlLeft': 57442, - 'ControlRight': 57448, - 'AltLeft': 57443, - 'AltRight': 57449, - 'MetaLeft': 57444, - 'MetaRight': 57450, - // Media keys - 'MediaPlayPause': 57430, - 'MediaStop': 57432, - 'MediaTrackNext': 57435, - 'MediaTrackPrevious': 57436, - 'AudioVolumeDown': 57438, - 'AudioVolumeUp': 57439, - 'AudioVolumeMute': 57440 -}; - -/** - * Keys that use CSI ~ encoding with a number parameter. - */ -const CSI_TILDE_KEYS: { [key: string]: number } = { - 'Insert': 2, - 'Delete': 3, - 'PageUp': 5, - 'PageDown': 6, - 'F5': 15, - 'F6': 17, - 'F7': 18, - 'F8': 19, - 'F9': 20, - 'F10': 21, - 'F11': 23, - 'F12': 24 -}; - -/** - * Keys that use CSI letter encoding (arrows, Home, End). - */ -const CSI_LETTER_KEYS: { [key: string]: string } = { - 'ArrowUp': 'A', - 'ArrowDown': 'B', - 'ArrowRight': 'C', - 'ArrowLeft': 'D', - 'Home': 'H', - 'End': 'F' -}; - -/** - * Function keys F1-F4 use SS3 encoding without modifiers. - */ -const SS3_FUNCTION_KEYS: { [key: string]: string } = { - 'F1': 'P', - 'F2': 'Q', - 'F3': 'R', - 'F4': 'S' -}; - -/** - * Map browser key codes to Kitty numpad codes. - */ -function getNumpadKeyCode(ev: IKeyboardEvent): number | undefined { - // Detect numpad via code property - if (ev.code.startsWith('Numpad')) { - const suffix = ev.code.slice(6); - if (suffix >= '0' && suffix <= '9') { - return 57399 + parseInt(suffix, 10); - } - switch (suffix) { - case 'Decimal': return 57409; - case 'Divide': return 57410; - case 'Multiply': return 57411; - case 'Subtract': return 57412; - case 'Add': return 57413; - case 'Enter': return 57414; - case 'Equal': return 57415; - } - } - return undefined; -} - -/** - * Get modifier key code from code property. - */ -function getModifierKeyCode(ev: IKeyboardEvent): number | undefined { - switch (ev.code) { - case 'ShiftLeft': return 57441; - case 'ShiftRight': return 57447; - case 'ControlLeft': return 57442; - case 'ControlRight': return 57448; - case 'AltLeft': return 57443; - case 'AltRight': return 57449; - case 'MetaLeft': return 57444; - case 'MetaRight': return 57450; - } - return undefined; -} - -/** - * Encode modifiers for Kitty protocol. - * Returns 1 + modifier bits, or 0 if no modifiers. - */ -function encodeModifiers(ev: IKeyboardEvent): number { - let mods = 0; - if (ev.shiftKey) mods |= KittyKeyboardModifiers.SHIFT; - if (ev.altKey) mods |= KittyKeyboardModifiers.ALT; - if (ev.ctrlKey) mods |= KittyKeyboardModifiers.CTRL; - if (ev.metaKey) mods |= KittyKeyboardModifiers.SUPER; - return mods > 0 ? mods + 1 : 0; -} - -/** - * Get the unicode key code for a keyboard event. - * Returns the lowercase codepoint for letters. - * For shifted keys, uses the code property to get the base key. - */ -function getKeyCode(ev: IKeyboardEvent): number | undefined { - // Check for numpad first - const numpadCode = getNumpadKeyCode(ev); - if (numpadCode !== undefined) { - return numpadCode; - } - - // Check for modifier keys - const modifierCode = getModifierKeyCode(ev); - if (modifierCode !== undefined) { - return modifierCode; - } - - // Check functional keys - const funcCode = FUNCTIONAL_KEY_CODES[ev.key]; - if (funcCode !== undefined) { - return funcCode; - } - - // For shifted keys, use code property to get base key - if (ev.shiftKey && ev.code) { - // Handle Digit0-Digit9 - if (ev.code.startsWith('Digit') && ev.code.length === 6) { - const digit = ev.code.charAt(5); - if (digit >= '0' && digit <= '9') { - return digit.charCodeAt(0); - } - } - // Handle KeyA-KeyZ - if (ev.code.startsWith('Key') && ev.code.length === 4) { - const letter = ev.code.charAt(3).toLowerCase(); - return letter.charCodeAt(0); - } - } - - // For regular keys, use the key character's codepoint - // Always use lowercase for letters (per spec) - if (ev.key.length === 1) { - const code = ev.key.codePointAt(0)!; - // Convert uppercase A-Z to lowercase a-z - if (code >= 65 && code <= 90) { - return code + 32; - } - return code; - } - - return undefined; -} - -/** - * Check if a key is a modifier key. - */ -function isModifierKey(ev: IKeyboardEvent): boolean { - return ev.key === 'Shift' || ev.key === 'Control' || ev.key === 'Alt' || ev.key === 'Meta'; -} - -/** - * Evaluate a keyboard event using Kitty keyboard protocol. - * - * @param ev The keyboard event. - * @param flags The active Kitty keyboard enhancement flags. - * @param eventType The event type (press, repeat, release). - * @returns The keyboard result with the encoded key sequence. - */ -export function evaluateKeyboardEventKitty( - ev: IKeyboardEvent, - flags: number, - eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS -): IKeyboardResult { - const result: IKeyboardResult = { - type: KeyboardResultType.SEND_KEY, - cancel: false, - key: undefined +export class KittyKeyboard { + /** + * Functional key codes for Kitty protocol. + * Keys that don't produce text have specific unicode codepoint mappings. + */ + private readonly _functionalKeyCodes: { [key: string]: number } = { + 'Escape': 27, + 'Enter': 13, + 'Tab': 9, + 'Backspace': 127, + 'CapsLock': 57358, + 'ScrollLock': 57359, + 'NumLock': 57360, + 'PrintScreen': 57361, + 'Pause': 57362, + 'ContextMenu': 57363, + // F13-F35 (F1-F12 use legacy encoding) + 'F13': 57376, + 'F14': 57377, + 'F15': 57378, + 'F16': 57379, + 'F17': 57380, + 'F18': 57381, + 'F19': 57382, + 'F20': 57383, + 'F21': 57384, + 'F22': 57385, + 'F23': 57386, + 'F24': 57387, + 'F25': 57388, + // Keypad keys + 'KP_0': 57399, + 'KP_1': 57400, + 'KP_2': 57401, + 'KP_3': 57402, + 'KP_4': 57403, + 'KP_5': 57404, + 'KP_6': 57405, + 'KP_7': 57406, + 'KP_8': 57407, + 'KP_9': 57408, + 'KP_Decimal': 57409, + 'KP_Divide': 57410, + 'KP_Multiply': 57411, + 'KP_Subtract': 57412, + 'KP_Add': 57413, + 'KP_Enter': 57414, + 'KP_Equal': 57415, + // Modifier keys + 'ShiftLeft': 57441, + 'ShiftRight': 57447, + 'ControlLeft': 57442, + 'ControlRight': 57448, + 'AltLeft': 57443, + 'AltRight': 57449, + 'MetaLeft': 57444, + 'MetaRight': 57450, + // Media keys + 'MediaPlayPause': 57430, + 'MediaStop': 57432, + 'MediaTrackNext': 57435, + 'MediaTrackPrevious': 57436, + 'AudioVolumeDown': 57438, + 'AudioVolumeUp': 57439, + 'AudioVolumeMute': 57440 }; - const modifiers = encodeModifiers(ev); - const isMod = isModifierKey(ev); - const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES); + /** + * Keys that use CSI ~ encoding with a number parameter. + */ + private readonly _csiTildeKeys: { [key: string]: number } = { + 'Insert': 2, + 'Delete': 3, + 'PageUp': 5, + 'PageDown': 6, + 'F5': 15, + 'F6': 17, + 'F7': 18, + 'F8': 19, + 'F9': 20, + 'F10': 21, + 'F11': 23, + 'F12': 24 + }; - // Don't report release events unless flag is set - if (!reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) { - return result; - } + /** + * Keys that use CSI letter encoding (arrows, Home, End). + */ + private readonly _csiLetterKeys: { [key: string]: string } = { + 'ArrowUp': 'A', + 'ArrowDown': 'B', + 'ArrowRight': 'C', + 'ArrowLeft': 'D', + 'Home': 'H', + 'End': 'F' + }; - // Modifier-only keys require REPORT_ALL_KEYS_AS_ESCAPE_CODES or REPORT_EVENT_TYPES - if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) { - return result; - } + /** + * Function keys F1-F4 use SS3 encoding without modifiers. + */ + private readonly _ss3FunctionKeys: { [key: string]: string } = { + 'F1': 'P', + 'F2': 'Q', + 'F3': 'R', + 'F4': 'S' + }; - // Check for CSI letter keys (arrows, Home, End) - const csiLetter = CSI_LETTER_KEYS[ev.key]; - if (csiLetter) { - result.key = buildCsiLetterSequence(csiLetter, modifiers, eventType, reportEventTypes); - result.cancel = true; - return result; - } - - // Check for SS3/CSI function keys (F1-F4) - const ss3Letter = SS3_FUNCTION_KEYS[ev.key]; - if (ss3Letter) { - result.key = buildSs3Sequence(ss3Letter, modifiers, eventType, reportEventTypes); - result.cancel = true; - return result; - } - - // Check for CSI ~ keys (Insert, Delete, PageUp/Down, F5-F12) - const tildeCode = CSI_TILDE_KEYS[ev.key]; - if (tildeCode !== undefined) { - result.key = buildCsiTildeSequence(tildeCode, modifiers, eventType, reportEventTypes); - result.cancel = true; - return result; - } - - // Get the key code for CSI u encoding - const keyCode = getKeyCode(ev); - if (keyCode === undefined) { - return result; - } - - const isFunc = FUNCTIONAL_KEY_CODES[ev.key] !== undefined || getNumpadKeyCode(ev) !== undefined; - - // Determine if we should use CSI u encoding - let useCsiU = false; - - if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) { - useCsiU = true; - } else if (reportEventTypes) { - useCsiU = true; - } else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) { - // Modifier-only keys already handled above - // Use CSI u for keys that would be ambiguous in legacy encoding - if (keyCode === 27 || keyCode === 127 || keyCode === 13 || keyCode === 9 || keyCode === 32) { - // Escape, Backspace, Enter, Tab, Space - useCsiU = true; - } else if (isFunc) { - useCsiU = true; - } else if (modifiers > 0) { - // Shift-only + printable character (e.g., Shift+a → "A") should NOT use CSI u - // per Kitty spec: DISAMBIGUATE only encodes keys ambiguous in legacy encoding - if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) { - useCsiU = false; - } else { - useCsiU = true; + /** + * Map browser key codes to Kitty numpad codes. + */ + private _getNumpadKeyCode(ev: IKeyboardEvent): number | undefined { + if (ev.code.startsWith('Numpad')) { + const suffix = ev.code.slice(6); + if (suffix >= '0' && suffix <= '9') { + return 57399 + parseInt(suffix, 10); + } + switch (suffix) { + case 'Decimal': return 57409; + case 'Divide': return 57410; + case 'Multiply': return 57411; + case 'Subtract': return 57412; + case 'Add': return 57413; + case 'Enter': return 57414; + case 'Equal': return 57415; } } + return undefined; } - if (useCsiU) { - result.key = buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod); - result.cancel = true; - } else { - // Legacy-compatible encoding for text keys without modifiers - if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) { - result.key = ev.key; + /** + * Get modifier key code from code property. + */ + private _getModifierKeyCode(ev: IKeyboardEvent): number | undefined { + switch (ev.code) { + case 'ShiftLeft': return 57441; + case 'ShiftRight': return 57447; + case 'ControlLeft': return 57442; + case 'ControlRight': return 57448; + case 'AltLeft': return 57443; + case 'AltRight': return 57449; + case 'MetaLeft': return 57444; + case 'MetaRight': return 57450; } + return undefined; } - return result; -} + /** + * Encode modifiers for Kitty protocol. + * Returns 1 + modifier bits, or 0 if no modifiers. + */ + private _encodeModifiers(ev: IKeyboardEvent): number { + let mods = 0; + if (ev.shiftKey) mods |= KittyKeyboardModifiers.SHIFT; + if (ev.altKey) mods |= KittyKeyboardModifiers.ALT; + if (ev.ctrlKey) mods |= KittyKeyboardModifiers.CTRL; + if (ev.metaKey) mods |= KittyKeyboardModifiers.SUPER; + return mods > 0 ? mods + 1 : 0; + } -/** - * Build CSI letter sequence for arrow keys, Home, End. - * Format: CSI [1;mod] letter - */ -function buildCsiLetterSequence( - letter: string, - modifiers: number, - eventType: KittyKeyboardEventType, - reportEventTypes: boolean -): string { - const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS; - - if (modifiers > 0 || needsEventType) { - let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1'); - if (needsEventType) { - seq += ':' + eventType; + /** + * Get the unicode key code for a keyboard event. + * Returns the lowercase codepoint for letters. + * For shifted keys, uses the code property to get the base key. + */ + private _getKeyCode(ev: IKeyboardEvent): number | undefined { + const numpadCode = this._getNumpadKeyCode(ev); + if (numpadCode !== undefined) { + return numpadCode; } - seq += letter; + + const modifierCode = this._getModifierKeyCode(ev); + if (modifierCode !== undefined) { + return modifierCode; + } + + const funcCode = this._functionalKeyCodes[ev.key]; + if (funcCode !== undefined) { + return funcCode; + } + + if (ev.shiftKey && ev.code) { + if (ev.code.startsWith('Digit') && ev.code.length === 6) { + const digit = ev.code.charAt(5); + if (digit >= '0' && digit <= '9') { + return digit.charCodeAt(0); + } + } + if (ev.code.startsWith('Key') && ev.code.length === 4) { + const letter = ev.code.charAt(3).toLowerCase(); + return letter.charCodeAt(0); + } + } + + if (ev.key.length === 1) { + const code = ev.key.codePointAt(0)!; + if (code >= 65 && code <= 90) { + return code + 32; + } + return code; + } + + return undefined; + } + + /** + * Check if a key is a modifier key. + */ + private _isModifierKey(ev: IKeyboardEvent): boolean { + return ev.key === 'Shift' || ev.key === 'Control' || ev.key === 'Alt' || ev.key === 'Meta'; + } + + /** + * Build CSI letter sequence for arrow keys, Home, End. + * Format: CSI [1;mod] letter + */ + private _buildCsiLetterSequence( + letter: string, + modifiers: number, + eventType: KittyKeyboardEventType, + reportEventTypes: boolean + ): string { + const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS; + + if (modifiers > 0 || needsEventType) { + let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1'); + if (needsEventType) { + seq += ':' + eventType; + } + seq += letter; + return seq; + } + return C0.ESC + '[' + letter; + } + + /** + * Build SS3 sequence for F1-F4. + * Without modifiers: SS3 letter + * With modifiers: CSI 1;mod letter + */ + private _buildSs3Sequence( + letter: string, + modifiers: number, + eventType: KittyKeyboardEventType, + reportEventTypes: boolean + ): string { + const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS; + + if (modifiers > 0 || needsEventType) { + let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1'); + if (needsEventType) { + seq += ':' + eventType; + } + seq += letter; + return seq; + } + return C0.ESC + 'O' + letter; + } + + /** + * Build CSI ~ sequence for Insert, Delete, PageUp/Down, F5-F12. + * Format: CSI number [;mod[:event]] ~ + */ + private _buildCsiTildeSequence( + number: number, + modifiers: number, + eventType: KittyKeyboardEventType, + reportEventTypes: boolean + ): string { + const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS; + + let seq = C0.ESC + '[' + number; + if (modifiers > 0 || needsEventType) { + seq += ';' + (modifiers > 0 ? modifiers : '1'); + if (needsEventType) { + seq += ':' + eventType; + } + } + seq += '~'; return seq; } - return C0.ESC + '[' + letter; -} -/** - * Build SS3 sequence for F1-F4. - * Without modifiers: SS3 letter - * With modifiers: CSI 1;mod letter - */ -function buildSs3Sequence( - letter: string, - modifiers: number, - eventType: KittyKeyboardEventType, - reportEventTypes: boolean -): string { - const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS; + /** + * Build CSI u sequence. + * Format: CSI keycode[:shifted[:base]] [;mod[:event][;text]] u + */ + private _buildCsiUSequence( + ev: IKeyboardEvent, + keyCode: number, + modifiers: number, + eventType: KittyKeyboardEventType, + flags: number, + isFunc: boolean, + isMod: boolean + ): string { + const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES); + const reportAlternateKeys = !!(flags & KittyKeyboardFlags.REPORT_ALTERNATE_KEYS); - if (modifiers > 0 || needsEventType) { - let seq = C0.ESC + '[1;' + (modifiers > 0 ? modifiers : '1'); - if (needsEventType) { - seq += ':' + eventType; + let seq = C0.ESC + '[' + keyCode; + + let shiftedKey: number | undefined; + if (reportAlternateKeys && ev.shiftKey && ev.key.length === 1 && !isFunc && !isMod) { + shiftedKey = ev.key.codePointAt(0); + seq += ':' + shiftedKey; } - seq += letter; + + const reportAssociatedText = !!(flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) && + eventType !== KittyKeyboardEventType.RELEASE && + ev.key.length === 1 && + !isFunc && + !isMod && + !ev.ctrlKey; + const textCode = reportAssociatedText ? ev.key.codePointAt(0) : undefined; + + const needsEventType = reportEventTypes && + eventType !== KittyKeyboardEventType.PRESS && + (eventType === KittyKeyboardEventType.RELEASE || textCode === undefined); + + if (modifiers > 0 || needsEventType || textCode !== undefined) { + seq += ';'; + if (modifiers > 0) { + seq += modifiers; + } else if (needsEventType) { + seq += '1'; + } + if (needsEventType) { + seq += ':' + eventType; + } + } + + if (textCode !== undefined) { + seq += ';' + textCode; + } + + seq += 'u'; return seq; } - return C0.ESC + 'O' + letter; -} -/** - * Build CSI ~ sequence for Insert, Delete, PageUp/Down, F5-F12. - * Format: CSI number [;mod[:event]] ~ - */ -function buildCsiTildeSequence( - number: number, - modifiers: number, - eventType: KittyKeyboardEventType, - reportEventTypes: boolean -): string { - const needsEventType = reportEventTypes && eventType !== KittyKeyboardEventType.PRESS; + /** + * Evaluate a keyboard event using Kitty keyboard protocol. + * + * @param ev The keyboard event. + * @param flags The active Kitty keyboard enhancement flags. + * @param eventType The event type (press, repeat, release). + * @returns The keyboard result with the encoded key sequence. + */ + public evaluate( + ev: IKeyboardEvent, + flags: number, + eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS + ): IKeyboardResult { + const result: IKeyboardResult = { + type: KeyboardResultType.SEND_KEY, + cancel: false, + key: undefined + }; - let seq = C0.ESC + '[' + number; - if (modifiers > 0 || needsEventType) { - seq += ';' + (modifiers > 0 ? modifiers : '1'); - if (needsEventType) { - seq += ':' + eventType; + const modifiers = this._encodeModifiers(ev); + const isMod = this._isModifierKey(ev); + const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES); + + if (!reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) { + return result; } - } - seq += '~'; - return seq; -} -/** - * Build CSI u sequence. - * Format: CSI keycode[:shifted[:base]] [;mod[:event][;text]] u - */ -function buildCsiUSequence( - ev: IKeyboardEvent, - keyCode: number, - modifiers: number, - eventType: KittyKeyboardEventType, - flags: number, - isFunc: boolean, - isMod: boolean -): string { - const reportEventTypes = !!(flags & KittyKeyboardFlags.REPORT_EVENT_TYPES); - const reportAlternateKeys = !!(flags & KittyKeyboardFlags.REPORT_ALTERNATE_KEYS); - - let seq = C0.ESC + '[' + keyCode; - - // Add shifted key alternate if REPORT_ALTERNATE_KEYS is set and shift is pressed - // Only for text-producing keys (not functional or modifier keys) - let shiftedKey: number | undefined; - if (reportAlternateKeys && ev.shiftKey && ev.key.length === 1 && !isFunc && !isMod) { - shiftedKey = ev.key.codePointAt(0); - seq += ':' + shiftedKey; - } - - // Check if we need associated text (press and repeat events, not release) - // Only for text-producing keys (not functional or modifier keys) - // Also don't include text when ctrl is pressed (produces control code) - const reportAssociatedText = !!(flags & KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT) && - eventType !== KittyKeyboardEventType.RELEASE && - ev.key.length === 1 && - !isFunc && - !isMod && - !ev.ctrlKey; - const textCode = reportAssociatedText ? ev.key.codePointAt(0) : undefined; - - // Determine if we need event type suffix - // For repeat: only include :2 when there's no text (text implies it's still useful input) - // For release: always include :3 - const needsEventType = reportEventTypes && - eventType !== KittyKeyboardEventType.PRESS && - (eventType === KittyKeyboardEventType.RELEASE || textCode === undefined); - - if (modifiers > 0 || needsEventType || textCode !== undefined) { - seq += ';'; - if (modifiers > 0) { - seq += modifiers; - } else if (needsEventType) { - seq += '1'; + if (isMod && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) && !reportEventTypes) { + return result; } - if (needsEventType) { - seq += ':' + eventType; + + const csiLetter = this._csiLetterKeys[ev.key]; + if (csiLetter) { + result.key = this._buildCsiLetterSequence(csiLetter, modifiers, eventType, reportEventTypes); + result.cancel = true; + return result; } + + const ss3Letter = this._ss3FunctionKeys[ev.key]; + if (ss3Letter) { + result.key = this._buildSs3Sequence(ss3Letter, modifiers, eventType, reportEventTypes); + result.cancel = true; + return result; + } + + const tildeCode = this._csiTildeKeys[ev.key]; + if (tildeCode !== undefined) { + result.key = this._buildCsiTildeSequence(tildeCode, modifiers, eventType, reportEventTypes); + result.cancel = true; + return result; + } + + const keyCode = this._getKeyCode(ev); + if (keyCode === undefined) { + return result; + } + + const isFunc = this._functionalKeyCodes[ev.key] !== undefined || this._getNumpadKeyCode(ev) !== undefined; + + let useCsiU = false; + + if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) { + useCsiU = true; + } else if (reportEventTypes) { + useCsiU = true; + } else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) { + if (keyCode === 27 || keyCode === 127 || keyCode === 13 || keyCode === 9 || keyCode === 32) { + useCsiU = true; + } else if (isFunc) { + useCsiU = true; + } else if (modifiers > 0) { + if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) { + useCsiU = false; + } else { + useCsiU = true; + } + } + } + + if (useCsiU) { + result.key = this._buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod); + result.cancel = true; + } else { + if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) { + result.key = ev.key; + } + } + + return result; } - // Add associated text if requested - if (textCode !== undefined) { - seq += ';' + textCode; + /** + * Check if Kitty protocol should be used based on flags. + */ + public static shouldUseProtocol(flags: number): boolean { + return flags > 0; } - - seq += 'u'; - return seq; -} - -/** - * Check if a keyboard event should be handled by Kitty protocol. - * Returns true if Kitty flags are active and the event should use Kitty encoding. - */ -export function shouldUseKittyProtocol(flags: number): boolean { - return flags > 0; } From b30d9c35336b71c5b89208846cf797ab9db3fd21 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:27:31 -0800 Subject: [PATCH 27/31] Remove trace decorator and unused platform consts --- addons/addon-webgl/src/WebglAddon.ts | 8 +------- src/common/Platform.ts | 2 -- src/common/services/LogService.ts | 30 ---------------------------- 3 files changed, 1 insertion(+), 39 deletions(-) diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 7dc3ed35..dd596be4 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -9,10 +9,9 @@ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRender import { ITerminal } from 'browser/Types'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { getSafariVersion, isSafari } from 'common/Platform'; -import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { IWebGL2RenderingContext } from './Types'; import { WebglRenderer } from './WebglRenderer'; -import { setTraceLogger } from 'common/services/LogService'; import { Emitter, EventUtils } from 'common/Event'; export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi { @@ -66,13 +65,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi const charSizeService: ICharSizeService = unsafeCore._charSizeService; const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; const decorationService: IDecorationService = unsafeCore._decorationService; - const logService: ILogService = unsafeCore._logService; const themeService: IThemeService = unsafeCore._themeService; - // Set trace logger just in case it hasn't been yet which could happen when the addon is - // bundled separately to the core module - setTraceLogger(logService); - this._renderer = this._register(new WebglRenderer( terminal, characterJoinerService, diff --git a/src/common/Platform.ts b/src/common/Platform.ts index ec34acde..cc13c1af 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -39,8 +39,6 @@ export function getSafariVersion(): number { // and ISO third level shifts. // http://stackoverflow.com/q/19877924/577598 export const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform); -export const isIpad = platform === 'iPad'; -export const isIphone = platform === 'iPhone'; export const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform); export const isLinux = platform.indexOf('Linux') >= 0; // Note that when this is true, isLinux will also be true. diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index 12c5e3b0..8bc1e0aa 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -43,9 +43,6 @@ export class LogService extends Disposable implements ILogService { super(); this._updateLogLevel(); this._register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel())); - - // For trace logging, assume the latest created log service is valid - traceLogger = this; } private _updateLogLevel(): void { @@ -95,30 +92,3 @@ export class LogService extends Disposable implements ILogService { } } } - -let traceLogger: ILogService; -export function setTraceLogger(logger: ILogService): void { - traceLogger = logger; -} - -/** - * A decorator that can be used to automatically log trace calls to the decorated function. - */ -export function traceCall(_target: any, key: string, descriptor: any): any { - if (typeof descriptor.value !== 'function') { - throw new Error('not supported'); - } - const fnKey = 'value'; - const fn = descriptor.value; - descriptor[fnKey] = function (...args: any[]) { - // Early exit - if (traceLogger.logLevel !== LogLevelEnum.TRACE) { - return fn.apply(this, args); - } - - traceLogger.trace(`GlyphRenderer#${fn.name}(${args.map(e => JSON.stringify(e)).join(', ')})`); - const result = fn.apply(this, args); - traceLogger.trace(`GlyphRenderer#${fn.name} return`, result); - return result; - }; -} From 5938b8823d9ef9e3ff73a655ccc5cf0443316265 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:38:16 -0800 Subject: [PATCH 28/31] Add @stylistic/comma-spacing eslint rule --- addons/addon-attach/src/AttachAddon.ts | 2 +- addons/addon-fit/src/FitAddon.ts | 2 +- addons/addon-image/src/ImageAddon.ts | 2 +- addons/addon-ligatures/src/LigaturesAddon.ts | 2 +- addons/addon-serialize/src/SerializeAddon.ts | 2 +- addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts | 2 +- addons/addon-unicode11/src/Unicode11Addon.ts | 2 +- addons/addon-web-links/src/WebLinksAddon.ts | 2 +- addons/addon-webgl/src/WebglAddon.ts | 2 +- eslint.config.mjs | 1 + src/browser/renderer/shared/RendererUtils.test.ts | 2 +- 11 files changed, 11 insertions(+), 10 deletions(-) diff --git a/addons/addon-attach/src/AttachAddon.ts b/addons/addon-attach/src/AttachAddon.ts index 37a10d17..9463dc36 100644 --- a/addons/addon-attach/src/AttachAddon.ts +++ b/addons/addon-attach/src/AttachAddon.ts @@ -12,7 +12,7 @@ interface IAttachOptions { bidirectional?: boolean; } -export class AttachAddon implements ITerminalAddon , IAttachApi { +export class AttachAddon implements ITerminalAddon, IAttachApi { private _socket: WebSocket; private _bidirectional: boolean; private _disposables: IDisposable[] = []; diff --git a/addons/addon-fit/src/FitAddon.ts b/addons/addon-fit/src/FitAddon.ts index 23004c1c..3b06fb73 100644 --- a/addons/addon-fit/src/FitAddon.ts +++ b/addons/addon-fit/src/FitAddon.ts @@ -33,7 +33,7 @@ function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration { return getWindow(el).getComputedStyle(el, null); } -export class FitAddon implements ITerminalAddon , IFitApi { +export class FitAddon implements ITerminalAddon, IFitApi { private _terminal: Terminal | undefined; public activate(terminal: Terminal): void { diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index 167e93a7..be505b83 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -48,7 +48,7 @@ const enum GaStatus { } -export class ImageAddon implements ITerminalAddon , IImageApi { +export class ImageAddon implements ITerminalAddon, IImageApi { private _opts: IImageAddonOptions; private _defaultOpts: IImageAddonOptions; private _storage: ImageStorage | undefined; diff --git a/addons/addon-ligatures/src/LigaturesAddon.ts b/addons/addon-ligatures/src/LigaturesAddon.ts index 2bde9cf6..63155474 100644 --- a/addons/addon-ligatures/src/LigaturesAddon.ts +++ b/addons/addon-ligatures/src/LigaturesAddon.ts @@ -13,7 +13,7 @@ export interface ITerminalAddon { dispose(): void; } -export class LigaturesAddon implements ITerminalAddon , ILigaturesApi { +export class LigaturesAddon implements ITerminalAddon, ILigaturesApi { private readonly _fallbackLigatures: string[]; private readonly _fontFeatureSettings?: string; diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index c9c66ce8..5dd77fc6 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -457,7 +457,7 @@ class StringSerializeHandler extends BaseSerializeHandler { } } -export class SerializeAddon implements ITerminalAddon , ISerializeApi { +export class SerializeAddon implements ITerminalAddon, ISerializeApi { private _terminal: Terminal | undefined; public activate(terminal: Terminal): void { diff --git a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts index 6e4d2266..a1509010 100644 --- a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts +++ b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts @@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm'; import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes'; import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider'; -export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi { +export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi { private _provider15Graphemes?: UnicodeGraphemeProvider; private _provider15?: UnicodeGraphemeProvider; private _unicode?: IUnicodeHandling; diff --git a/addons/addon-unicode11/src/Unicode11Addon.ts b/addons/addon-unicode11/src/Unicode11Addon.ts index 301145ee..897f4f40 100644 --- a/addons/addon-unicode11/src/Unicode11Addon.ts +++ b/addons/addon-unicode11/src/Unicode11Addon.ts @@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon } from '@xterm/xterm'; import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11'; import { UnicodeV11 } from './UnicodeV11'; -export class Unicode11Addon implements ITerminalAddon , IUnicode11Api { +export class Unicode11Addon implements ITerminalAddon, IUnicode11Api { public activate(terminal: Terminal): void { terminal.unicode.register(new UnicodeV11()); } diff --git a/addons/addon-web-links/src/WebLinksAddon.ts b/addons/addon-web-links/src/WebLinksAddon.ts index b3f0548c..d32668b6 100644 --- a/addons/addon-web-links/src/WebLinksAddon.ts +++ b/addons/addon-web-links/src/WebLinksAddon.ts @@ -35,7 +35,7 @@ function handleLink(event: MouseEvent, uri: string): void { } } -export class WebLinksAddon implements ITerminalAddon , IWebLinksApi { +export class WebLinksAddon implements ITerminalAddon, IWebLinksApi { private _terminal: Terminal | undefined; private _linkProvider: IDisposable | undefined; diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 7dc3ed35..60a05e55 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -15,7 +15,7 @@ import { WebglRenderer } from './WebglRenderer'; import { setTraceLogger } from 'common/services/LogService'; import { Emitter, EventUtils } from 'common/Event'; -export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi { +export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi { private _terminal?: Terminal; private _renderer?: WebglRenderer; diff --git a/eslint.config.mjs b/eslint.config.mjs index 6cf78ecd..68422f9d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -35,6 +35,7 @@ export default tseslint.config( } }, rules: { + '@stylistic/comma-spacing': ['warn', { before: false, after: true }], '@stylistic/indent': ['warn', 2], '@stylistic/semi': ['warn', 'always'], '@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }], diff --git a/src/browser/renderer/shared/RendererUtils.test.ts b/src/browser/renderer/shared/RendererUtils.test.ts index a050e8a9..4512cb6d 100644 --- a/src/browser/renderer/shared/RendererUtils.test.ts +++ b/src/browser/renderer/shared/RendererUtils.test.ts @@ -28,7 +28,7 @@ describe('RendererUtils', () => { line = 2; variantOffset = 0; cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth]; - result = [3, 2, 0 ,2]; + result = [3, 2, 0, 2]; for (let index = 0; index < cells.length; index++) { const cell = cells[index]; variantOffset = computeNextVariantOffset(cell, line, variantOffset); From bad7e329df2c844270666f622a7f25122d33e8c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:43:05 -0800 Subject: [PATCH 29/31] Add @typescript-eslint/prefer-nullish-coalescing eslint rule --- addons/addon-image/src/ImageAddon.ts | 2 +- addons/addon-image/src/ImageRenderer.ts | 4 +- addons/addon-image/src/ImageStorage.ts | 10 ++-- addons/addon-ligatures/src/LigaturesAddon.ts | 2 +- addons/addon-ligatures/src/font.ts | 4 +- .../src/fontLigatures/processors/helper.ts | 20 +++---- .../fontLigatures/processors/substitution.ts | 5 +- addons/addon-search/src/SearchEngine.ts | 4 +- .../src/SerializeAddon.test.ts | 50 ++++++++--------- addons/addon-serialize/src/SerializeAddon.ts | 2 +- .../src/UnicodeGraphemesAddon.ts | 8 +-- addons/addon-web-links/src/WebLinksAddon.ts | 2 +- addons/addon-webgl/src/TextureAtlas.ts | 2 +- eslint.config.mjs | 8 +++ src/browser/CoreBrowserTerminal.ts | 8 +-- src/browser/RenderDebouncer.ts | 4 +- src/browser/TimeBasedDebouncer.ts | 4 +- src/browser/public/Terminal.ts | 10 +--- .../renderer/dom/DomRendererRowFactory.ts | 4 +- src/browser/services/RenderService.ts | 12 ++--- src/common/Event.ts | 53 ++++++++++--------- src/common/InputHandler.ts | 2 +- src/common/buffer/Buffer.ts | 12 ++--- src/common/buffer/BufferLine.ts | 2 +- src/common/input/Keyboard.test.ts | 2 +- src/common/input/KittyKeyboard.test.ts | 2 +- src/common/input/UnicodeV6.test.ts | 2 +- src/common/parser/ApcParser.ts | 4 +- src/common/parser/DcsParser.ts | 4 +- src/common/parser/EscapeSequenceParser.ts | 8 +-- src/common/parser/OscParser.ts | 4 +- src/headless/public/Terminal.ts | 8 +-- 32 files changed, 117 insertions(+), 151 deletions(-) diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index 167e93a7..6c7fe5ab 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -90,7 +90,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi { // windowOptions.getCellSizePixels = true; // windowOptions.getWinSizeChars = true; // terminal.setOption('windowOptions', windowOptions); - const windowOps = terminal.options.windowOptions || {}; + const windowOps = terminal.options.windowOptions ?? {}; windowOps.getWinSizePixels = true; windowOps.getCellSizePixels = true; windowOps.getWinSizeChars = true; diff --git a/addons/addon-image/src/ImageRenderer.ts b/addons/addon-image/src/ImageRenderer.ts index e37169f3..1e87aa97 100644 --- a/addons/addon-image/src/ImageRenderer.ts +++ b/addons/addon-image/src/ImageRenderer.ts @@ -38,7 +38,7 @@ export class ImageRenderer extends Disposable implements IDisposable { * Only the DOM output canvas should be on the terminal's document, * which gets explicitly checked in `insertLayerToDom`. */ - const canvas = (localDocument || document).createElement('canvas'); + const canvas = (localDocument ?? document).createElement('canvas'); canvas.width = width | 0; canvas.height = height | 0; return canvas; @@ -242,7 +242,7 @@ export class ImageRenderer extends Disposable implements IDisposable { } if (!this._placeholder) return; this._ctx.drawImage( - this._placeholderBitmap || this._placeholder!, + this._placeholderBitmap ?? this._placeholder!, col * width, (row * height) % 2 ? 0 : 1, // needs %2 offset correction width * count, diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index f9b6eef6..059849ea 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -381,7 +381,7 @@ export class ImageStorage implements IDisposable { if (!line) return; for (let col = 0; col < cols; ++col) { if (line.getBg(col) & BgFlags.HAS_EXTENDED) { - let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS; + let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS; const imageId = e.imageId; if (imageId === undefined || imageId === -1) { continue; @@ -400,7 +400,7 @@ export class ImageStorage implements IDisposable { while ( ++col < cols && (line.getBg(col) & BgFlags.HAS_EXTENDED) - && (e = line._extendedAttrs[col] || EMPTY_ATTRS) + && (e = line._extendedAttrs[col] ?? EMPTY_ATTRS) && (e.imageId === imageId) && (e.tileId === startTile + count) ) { @@ -442,7 +442,7 @@ export class ImageStorage implements IDisposable { for (let row = 0; row < rows; ++row) { const line = buffer.lines.get(row) as IBufferLineExt; if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS; + const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS; const imageId = e.imageId; if (imageId === undefined || imageId === -1) { continue; @@ -487,7 +487,7 @@ export class ImageStorage implements IDisposable { const buffer = this._terminal._core.buffer; const line = buffer.lines.get(y) as IBufferLineExt; if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS; + const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; if (e.imageId && e.imageId !== -1) { const orig = this._images.get(e.imageId)?.orig; if (window.ImageBitmap && orig instanceof ImageBitmap) { @@ -507,7 +507,7 @@ export class ImageStorage implements IDisposable { const buffer = this._terminal._core.buffer; const line = buffer.lines.get(y) as IBufferLineExt; if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS; + const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; if (e.imageId && e.imageId !== -1 && e.tileId !== -1) { const spec = this._images.get(e.imageId); if (spec) { diff --git a/addons/addon-ligatures/src/LigaturesAddon.ts b/addons/addon-ligatures/src/LigaturesAddon.ts index 2bde9cf6..463b0d26 100644 --- a/addons/addon-ligatures/src/LigaturesAddon.ts +++ b/addons/addon-ligatures/src/LigaturesAddon.ts @@ -22,7 +22,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi { constructor(options?: Partial) { // Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations - this._fallbackLigatures = (options?.fallbackLigatures || [ + this._fallbackLigatures = (options?.fallbackLigatures ?? [ '<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->', '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=', '<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::', diff --git a/addons/addon-ligatures/src/font.ts b/addons/addon-ligatures/src/font.ts index 196651ac..1316932a 100644 --- a/addons/addon-ligatures/src/font.ts +++ b/addons/addon-ligatures/src/font.ts @@ -80,9 +80,7 @@ export default async function load(fontFamily: string, cacheSize: number): Promi console.error(err.name, err.message); } } - if (!fontsPromise) { - fontsPromise = Promise.resolve({}); - } + fontsPromise ??= Promise.resolve({}); } const fonts = await fontsPromise; diff --git a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts index 3f06d672..e6259b6d 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts @@ -52,12 +52,10 @@ export function processLookaheadPosition( } processedEntries.add(currentEntry.entry); - if (!currentEntry.entry.forward) { - currentEntry.entry.forward = { - individual: {}, - range: [] - }; - } + currentEntry.entry.forward ??= { + individual: {}, + range: [] + }; // All glyphs at this position share ONE entry - lookahead just needs to match, // all paths lead to the same result @@ -97,12 +95,10 @@ export function processBacktrackPosition( } processedEntries.add(currentEntry.entry); - if (!currentEntry.entry.reverse) { - currentEntry.entry.reverse = { - individual: {}, - range: [] - }; - } + currentEntry.entry.reverse ??= { + individual: {}, + range: [] + }; // All glyphs at this position share ONE entry - backtrack just needs to match, // all paths lead to the same result diff --git a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts index 5eae2476..3a41e936 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts @@ -54,9 +54,6 @@ export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId return (glyphId + table.deltaGlyphId) % (2 ** 16); // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2 case 2: - // eslint-disable-next-line eqeqeq - return table.substitute[coverageIndex] != null - ? table.substitute[coverageIndex] - : null; + return table.substitute[coverageIndex] ?? null; } } diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index a78be61e..b9991974 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -198,9 +198,7 @@ export class SearchEngine { } } - if (!result) { - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - } + result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch); // Search from startRow - 1 to top if (!result) { diff --git a/addons/addon-serialize/src/SerializeAddon.test.ts b/addons/addon-serialize/src/SerializeAddon.test.ts index e5307e1a..6e8c2ad4 100644 --- a/addons/addon-serialize/src/SerializeAddon.test.ts +++ b/addons/addon-serialize/src/SerializeAddon.test.ts @@ -228,7 +228,7 @@ describe('SerializeAddon', () => { it('empty terminal with selection turned off', () => { const output = serializeAddon.serializeAsHTML(); assert.notEqual(output, ''); - assert.equal((output.match(/
{10}<\/span><\/div>/g) || []).length, 2); + assert.equal((output.match(/
{10}<\/span><\/div>/g) ?? []).length, 2); }); it('empty terminal with no selection', () => { @@ -245,7 +245,7 @@ describe('SerializeAddon', () => { const output = serializeAddon.serializeAsHTML({ onlySelection: true }); - assert.equal((output.match(/
terminal<\/span><\/div>/g) || []).length, 1, output); + assert.equal((output.match(/
terminal<\/span><\/div>/g) ?? []).length, 1, output); }); it('basic terminal with html unsafe chars', async () => { @@ -255,7 +255,7 @@ describe('SerializeAddon', () => { const output = serializeAddon.serializeAsHTML({ onlySelection: true }); - assert.equal((output.match(/
<a>&pi;<\/span><\/div>/g) || []).length, 1, output); + assert.equal((output.match(/
<a>&pi;<\/span><\/div>/g) ?? []).length, 1, output); }); it('serializes rows within a provided range', async () => { @@ -267,7 +267,7 @@ describe('SerializeAddon', () => { startCol: 4 } }); - const rowMatches = output.match(/
.*?<\/span><\/div>/g) || []; + const rowMatches = output.match(/
.*?<\/span><\/div>/g) ?? []; assert.equal(rowMatches.length, 1, output); assert.ok(rowMatches[0]?.includes('hello')); assert.ok(!output.includes('bye')); @@ -278,56 +278,56 @@ describe('SerializeAddon', () => { await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with italic styling', async () => { await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with inverse styling', async () => { await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with underline styling', async () => { await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with double underline styling', async () => { await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with curly underline styling', async () => { await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with dotted underline styling', async () => { await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with dashed underline styling', async () => { await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with underline color (palette)', async () => { @@ -350,49 +350,49 @@ describe('SerializeAddon', () => { await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with dim styling', async () => { await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with strikethrough styling', async () => { await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with combined styling', async () => { await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/ <\/span>/g) || []).length, 1, output); - assert.equal((output.match(/termi<\/span>/g) || []).length, 1, output); - assert.equal((output.match(/nal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/ <\/span>/g) ?? []).length, 1, output); + assert.equal((output.match(/termi<\/span>/g) ?? []).length, 1, output); + assert.equal((output.match(/nal<\/span>/g) ?? []).length, 1, output); }); it('cells with color styling', async () => { await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with background styling', async () => { await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('empty terminal with default options', async () => { const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output); + assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output); }); it('empty terminal with custom options', async () => { @@ -405,14 +405,14 @@ describe('SerializeAddon', () => { const output = serializeAddon.serializeAsHTML({ includeGlobalBackground: true }); - assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output); + assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) ?? []).length, 1, output); }); it('empty terminal with background included', async () => { const output = serializeAddon.serializeAsHTML({ includeGlobalBackground: true }); - assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output); + assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output); }); it('cells with custom color styling', async () => { @@ -422,7 +422,7 @@ describe('SerializeAddon', () => { await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); it('cells with color styling - xterm headless', async () => { @@ -432,7 +432,7 @@ describe('SerializeAddon', () => { await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/terminal<\/span>/g) ?? []).length, 1, output); }); }); }); diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index c9c66ce8..4b06ae70 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -601,7 +601,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { throw new Error('Cannot use addon until it has been loaded'); } - return this._serializeBufferAsHTML(this._terminal, options || {}); + return this._serializeBufferAsHTML(this._terminal, options ?? {}); } public dispose(): void { } diff --git a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts index 6e4d2266..e40f2076 100644 --- a/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts +++ b/addons/addon-unicode-graphemes/src/UnicodeGraphemesAddon.ts @@ -16,12 +16,8 @@ export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemes private _oldVersion: string = ''; public activate(terminal: Terminal): void { - if (! this._provider15) { - this._provider15 = new UnicodeGraphemeProvider(false); - } - if (! this._provider15Graphemes) { - this._provider15Graphemes = new UnicodeGraphemeProvider(true); - } + this._provider15 ??= new UnicodeGraphemeProvider(false); + this._provider15Graphemes ??= new UnicodeGraphemeProvider(true); const unicode = terminal.unicode; this._unicode = unicode; unicode.register(this._provider15); diff --git a/addons/addon-web-links/src/WebLinksAddon.ts b/addons/addon-web-links/src/WebLinksAddon.ts index b3f0548c..56491e85 100644 --- a/addons/addon-web-links/src/WebLinksAddon.ts +++ b/addons/addon-web-links/src/WebLinksAddon.ts @@ -48,7 +48,7 @@ export class WebLinksAddon implements ITerminalAddon , IWebLinksApi { public activate(terminal: Terminal): void { this._terminal = terminal; const options = this._options as ILinkProviderOptions; - const regex = options.urlRegex || strictUrlRegex; + const regex = options.urlRegex ?? strictUrlRegex; this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options)); } diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 3486b2e0..aa0e2653 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -410,7 +410,7 @@ export class TextureAtlas implements ITextureAtlas { const cache = this._getContrastCache(dim); const adjustedColor = cache.getColor(bg, fg); if (adjustedColor !== undefined) { - return adjustedColor || undefined; + return adjustedColor ?? undefined; } const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse); diff --git a/eslint.config.mjs b/eslint.config.mjs index 6cf78ecd..d630c9dc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -66,6 +66,7 @@ export default tseslint.config( ], '@typescript-eslint/no-confusing-void-expression': ['warn', { ignoreArrowShorthand: true }], '@typescript-eslint/no-useless-constructor': 'warn', + '@typescript-eslint/prefer-nullish-coalescing': ['warn', { ignorePrimitives: true }], '@typescript-eslint/prefer-namespace-keyword': 'warn', '@typescript-eslint/no-unused-vars': ['warn', { vars: 'all', args: 'none' }], '@typescript-eslint/no-require-imports': 'off', @@ -140,5 +141,12 @@ export default tseslint.config( '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-member-accessibility': 'off' } + }, + { + // Disable prefer-nullish-coalescing for files without strictNullChecks + files: ['demo/**/*.ts', '**/*.benchmark.ts'], + rules: { + '@typescript-eslint/prefer-nullish-coalescing': 'off' + } } ); diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 869a545a..bb45cb64 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -793,15 +793,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (!(events & CoreMouseEventType.UP)) { this._document!.removeEventListener('mouseup', requestedEvents.mouseup!); requestedEvents.mouseup = null; - } else if (!requestedEvents.mouseup) { - requestedEvents.mouseup = eventListeners.mouseup; + } else { + requestedEvents.mouseup ??= eventListeners.mouseup; } if (!(events & CoreMouseEventType.DRAG)) { this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!); requestedEvents.mousedrag = null; - } else if (!requestedEvents.mousedrag) { - requestedEvents.mousedrag = eventListeners.mousedrag; + } else { + requestedEvents.mousedrag ??= eventListeners.mousedrag; } })); // force initial onProtocolChange so we dont miss early mouse requests diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index dd3b97a6..c7ddb6ce 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -40,8 +40,8 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { this._rowCount = rowCount; // Get the min/max row start/end for the arg values - rowStart = rowStart !== undefined ? rowStart : 0; - rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; + rowStart = rowStart ?? 0; + rowEnd = rowEnd ?? this._rowCount - 1; // Set the properties to the updated values this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts index 4d7a65a1..99731290 100644 --- a/src/browser/TimeBasedDebouncer.ts +++ b/src/browser/TimeBasedDebouncer.ts @@ -37,8 +37,8 @@ export class TimeBasedDebouncer implements IRenderDebouncer { public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { this._rowCount = rowCount; // Get the min/max row start/end for the arg values - rowStart = rowStart !== undefined ? rowStart : 0; - rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; + rowStart = rowStart ?? 0; + rowEnd = rowEnd ?? this._rowCount - 1; // Set the properties to the updated values this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 6b4cdee2..4a77fd4a 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -84,10 +84,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { - if (!this._parser) { - this._parser = new ParserApi(this._core); - } - return this._parser; + return this._parser ??= new ParserApi(this._core); } public get unicode(): IUnicodeHandling { this._checkProposedApi(); @@ -97,10 +94,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } public get buffer(): IBufferNamespaceApi { - if (!this._buffer) { - this._buffer = this._register(new BufferNamespaceApi(this._core)); - } - return this._buffer; + return this._buffer ??= this._register(new BufferNamespaceApi(this._core)); } public get markers(): ReadonlyArray { return this._core.markers; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 96d3f171..e95d221b 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -494,8 +494,8 @@ export class DomRendererRowFactory { // Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from // non-dim cells const ratio = this._optionsService.rawOptions.minimumContrastRatio / (cell.isDim() ? 2 : 1); - adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, ratio); - cache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); + adjustedColor = color.ensureContrastRatio(bgOverride ?? bg, fgOverride ?? fg, ratio); + cache.setColor((bgOverride ?? bg).rgba, (fgOverride ?? fg).rgba, adjustedColor ?? null); } if (adjustedColor) { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 1cced406..848eebcc 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -347,13 +347,11 @@ class SynchronizedOutputHandler { this._end = Math.max(this._end, end); } - if (this._timeout === undefined) { - this._timeout = this._coreBrowserService.window.setTimeout(() => { - this._timeout = undefined; - this._coreService.decPrivateModes.synchronizedOutput = false; - this._onTimeout(); - }, Constants.SYNCHRONIZED_OUTPUT_TIMEOUT_MS); - } + this._timeout ??= this._coreBrowserService.window.setTimeout(() => { + this._timeout = undefined; + this._coreService.decPrivateModes.synchronizedOutput = false; + this._onTimeout(); + }, Constants.SYNCHRONIZED_OUTPUT_TIMEOUT_MS); } public flush(): { start: number, end: number } | undefined { diff --git a/src/common/Event.ts b/src/common/Event.ts index 9ea39b9c..f9fcb4e7 100644 --- a/src/common/Event.ts +++ b/src/common/Event.ts @@ -18,33 +18,34 @@ export class Emitter { private _event: IEvent | undefined; public get event(): IEvent { - if (!this._event) { - this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { - if (this._disposed) { - return toDisposable(() => {}); - } - - const entry = { fn: listener, thisArgs }; - this._listeners.push(entry); - - const result = toDisposable(() => { - const idx = this._listeners.indexOf(entry); - if (idx !== -1) { - this._listeners.splice(idx, 1); - } - }); - - if (disposables) { - if (Array.isArray(disposables)) { - disposables.push(result); - } else { - disposables.add(result); - } - } - - return result; - }; + if (this._event) { + return this._event; } + this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { + if (this._disposed) { + return toDisposable(() => {}); + } + + const entry = { fn: listener, thisArgs }; + this._listeners.push(entry); + + const result = toDisposable(() => { + const idx = this._listeners.indexOf(entry); + if (idx !== -1) { + this._listeners.splice(idx, 1); + } + }); + + if (disposables) { + if (Array.isArray(disposables)) { + disposables.push(result); + } else { + disposables.add(result); + } + } + + return result; + }; return this._event; } diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index ed74520d..1bb923e6 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -3343,7 +3343,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (collectAndFlag[0] === '/') { return true; // TODO: Is this supported? } - this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] ?? DEFAULT_CHARSET); return true; } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index ee0374f7..b738ffef 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -122,9 +122,7 @@ export class Buffer implements IBuffer { */ public fillViewportRows(fillAttr?: IAttributeData): void { if (this.lines.length === 0) { - if (fillAttr === undefined) { - fillAttr = DEFAULT_ATTR_DATA; - } + fillAttr ??= DEFAULT_ATTR_DATA; let i = this._rows; while (i--) { this.lines.push(this.getBlankLine(fillAttr)); @@ -589,9 +587,7 @@ export class Buffer implements IBuffer { * @param x The position to move the cursor to the previous tab stop. */ public prevStop(x?: number): number { - if (x === null || x === undefined) { - x = this.x; - } + x ??= this.x; while (!this.tabs[--x] && x > 0); return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } @@ -601,9 +597,7 @@ export class Buffer implements IBuffer { * @param x The position to move the cursor one tab stop forward. */ public nextStop(x?: number): number { - if (x === null || x === undefined) { - x = this.x; - } + x ??= this.x; while (!this.tabs[++x] && x < this._cols); return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index ee3481a2..03177076 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -66,7 +66,7 @@ export class BufferLine implements IBufferLine { constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { this._data = new Uint32Array(cols * CELL_SIZE); - const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let i = 0; i < cols; ++i) { this.setCell(i, cell); } diff --git a/src/common/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts index 8c7f0dbc..47f516f0 100644 --- a/src/common/input/Keyboard.test.ts +++ b/src/common/input/Keyboard.test.ts @@ -26,7 +26,7 @@ function testEvaluateKeyboardEvent(partialEvent: { ctrlKey: partialEvent.ctrlKey || false, shiftKey: partialEvent.shiftKey || false, metaKey: partialEvent.metaKey || false, - keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0, + keyCode: partialEvent.keyCode ?? 0, code: partialEvent.code || '', key: partialEvent.key || '', type: partialEvent.type || '' diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts index e346708f..706eec15 100644 --- a/src/common/input/KittyKeyboard.test.ts +++ b/src/common/input/KittyKeyboard.test.ts @@ -9,7 +9,7 @@ function createEvent(partialEvent: Partial = {}): IKeyboardEvent ctrlKey: partialEvent.ctrlKey || false, shiftKey: partialEvent.shiftKey || false, metaKey: partialEvent.metaKey || false, - keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0, + keyCode: partialEvent.keyCode ?? 0, code: partialEvent.code || '', key: partialEvent.key || '', type: partialEvent.type || 'keydown' diff --git a/src/common/input/UnicodeV6.test.ts b/src/common/input/UnicodeV6.test.ts index 9f89048c..98dc97dc 100644 --- a/src/common/input/UnicodeV6.test.ts +++ b/src/common/input/UnicodeV6.test.ts @@ -169,7 +169,7 @@ it('wcwidth should match all values from the old implementation', function(): vo if (num < 127) { return 1; } - const t = table || initTable(); + const t = table ?? initTable(); if (num < 65536) { return t[num >> 4] >> ((num & 15) << 1) & 3; } diff --git a/src/common/parser/ApcParser.ts b/src/common/parser/ApcParser.ts index 81f26860..e5a82ce3 100644 --- a/src/common/parser/ApcParser.ts +++ b/src/common/parser/ApcParser.ts @@ -36,9 +36,7 @@ export class ApcParser implements IApcParser { * @param handler The handler to register */ public registerHandler(ident: number, handler: IApcHandler): IDisposable { - if (this._handlers[ident] === undefined) { - this._handlers[ident] = []; - } + this._handlers[ident] ??= []; const handlerList = this._handlers[ident]; handlerList.push(handler); return { diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index b66524ba..182179b0 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -29,9 +29,7 @@ export class DcsParser implements IDcsParser { } public registerHandler(ident: number, handler: IDcsHandler): IDisposable { - if (this._handlers[ident] === undefined) { - this._handlers[ident] = []; - } + this._handlers[ident] ??= []; const handlerList = this._handlers[ident]; handlerList.push(handler); return { diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index c192be43..bb99efe1 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -362,9 +362,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { const ident = this._identifier(id, [0x30, 0x7e]); - if (this._escHandlers[ident] === undefined) { - this._escHandlers[ident] = []; - } + this._escHandlers[ident] ??= []; const handlerList = this._escHandlers[ident]; handlerList.push(handler); return { @@ -395,9 +393,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { const ident = this._identifier(id); - if (this._csiHandlers[ident] === undefined) { - this._csiHandlers[ident] = []; - } + this._csiHandlers[ident] ??= []; const handlerList = this._csiHandlers[ident]; handlerList.push(handler); return { diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 32710aed..2a829a8e 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -23,9 +23,7 @@ export class OscParser implements IOscParser { }; public registerHandler(ident: number, handler: IOscHandler): IDisposable { - if (this._handlers[ident] === undefined) { - this._handlers[ident] = []; - } + this._handlers[ident] ??= []; const handlerList = this._handlers[ident]; handlerList.push(handler); return { diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index cdb94180..72614933 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -84,9 +84,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get onWriteParsed(): IEvent { return this._core.onWriteParsed; } public get parser(): IParser { - if (!this._parser) { - this._parser = new ParserApi(this._core); - } + this._parser ??= new ParserApi(this._core); return this._parser; } public get unicode(): IUnicodeHandling { @@ -96,9 +94,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } public get buffer(): IBufferNamespaceApi { - if (!this._buffer) { - this._buffer = this._register(new BufferNamespaceApi(this._core)); - } + this._buffer ??= this._register(new BufferNamespaceApi(this._core)); return this._buffer; } public get markers(): ReadonlyArray { From 1d322c9e453d41696e32772a71ef26ffb3262384 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:54:15 -0800 Subject: [PATCH 30/31] Fix name to follow enum rules --- src/browser/CoreBrowserTerminal.ts | 4 ++-- src/common/data/EscapeSequences.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 869a545a..57208160 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -48,7 +48,7 @@ import * as Browser from 'common/Platform'; import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBuffer } from 'common/buffer/Types'; -import { C0, C1_ESCAPED } from 'common/data/EscapeSequences'; +import { C0, C1ESCAPED } from 'common/data/EscapeSequences'; import { toRgbString } from 'common/input/XParseColor'; import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; @@ -235,7 +235,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { const colorRgb = color.toColorRGB(acc === 'ansi' ? this._themeService.colors.ansi[req.index] : this._themeService.colors[acc]); - this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`); + this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1ESCAPED.ST}`); break; case ColorRequestType.SET: if (acc === 'ansi') { diff --git a/src/common/data/EscapeSequences.ts b/src/common/data/EscapeSequences.ts index 2f60125f..fe6cf329 100644 --- a/src/common/data/EscapeSequences.ts +++ b/src/common/data/EscapeSequences.ts @@ -149,6 +149,6 @@ export const enum C1 { APC = '\x9f' } -export const enum C1_ESCAPED { +export const enum C1ESCAPED { ST = '\x1b\\' } From 0e679ea6aed689ef3daea9ea379e2977483ee2c0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:56:53 -0800 Subject: [PATCH 31/31] Use nullish coalescing in KeyboardService --- src/browser/services/KeyboardService.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index d9e6c1ee..a6150df3 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -24,16 +24,12 @@ export class KeyboardService implements IKeyboardService { } private _getWin32InputMode(): Win32InputMode { - if (!this._win32InputMode) { - this._win32InputMode = new Win32InputMode(); - } + this._win32InputMode ??= new Win32InputMode(); return this._win32InputMode; } private _getKittyKeyboard(): KittyKeyboard { - if (!this._kittyKeyboard) { - this._kittyKeyboard = new KittyKeyboard(); - } + this._kittyKeyboard ??= new KittyKeyboard(); return this._kittyKeyboard; }