From 2c1441a52d8a1b5a8fe418195892848b43a36308 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Jan 2026 10:49:21 -0800 Subject: [PATCH 01/49] 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 02/49] 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 03/49] 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 04/49] 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 6f87ae77a771b73b974a92165333a2b5314c03f7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:19:26 -0800 Subject: [PATCH 05/49] Support color scheme reporting CSI ? 996 n CSI ? 2031 h CSI ? 2031 l Fixes #5626 --- src/browser/CoreBrowserTerminal.ts | 26 +++++++++++++++++++++++++- src/common/InputHandler.test.ts | 20 ++++++++++++++++++++ src/common/InputHandler.ts | 18 ++++++++++++++++++ src/common/TestUtils.test.ts | 1 + src/common/Types.ts | 1 + src/common/services/CoreService.ts | 1 + src/common/services/Services.ts | 1 + test/playwright/InputHandler.test.ts | 26 ++++++++++++++++++++++++-- typings/xterm-headless.d.ts | 14 +++++++++++++- typings/xterm.d.ts | 14 +++++++++++++- 10 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index c26e6705..42da63db 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -42,7 +42,7 @@ import { SelectionService } from 'browser/services/SelectionService'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { ThemeService } from 'browser/services/ThemeService'; import { KeyboardService } from 'browser/services/KeyboardService'; -import { channels, color } from 'common/Color'; +import { channels, color, rgb } from 'common/Color'; import { CoreTerminal } from 'common/CoreTerminal'; import * as Browser from 'common/Platform'; import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types'; @@ -252,6 +252,20 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } } + /** + * Reports the current color scheme (dark or light) based on the relative luminance + * of the background and foreground theme colors. + * Sends CSI ? 997 ; 1 n for dark mode or CSI ? 997 ; 2 n for light mode. + */ + private _reportColorScheme(): void { + if (!this._themeService) return; + const bgLuminance = rgb.relativeLuminance(this._themeService.colors.background.rgba >> 8); + const fgLuminance = rgb.relativeLuminance(this._themeService.colors.foreground.rgba >> 8); + // Dark mode = background is darker than foreground (lower luminance) + const colorSchemeMode = bgLuminance < fgLuminance ? 1 : 2; + this.coreService.triggerDataEvent(`${C0.ESC}[?997;${colorSchemeMode}n`); + } + protected _setup(): void { super._setup(); @@ -495,6 +509,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._themeService = this._instantiationService.createInstance(ThemeService); this._instantiationService.setService(IThemeService, this._themeService); + // CSI ? 996 n - color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + this._register(this._inputHandler.onRequestColorSchemeQuery(() => this._reportColorScheme())); + + // Emit unsolicited color scheme notification on theme change when DECSET 2031 is enabled + this._register(this._themeService.onChangeColors(() => { + if (this.coreService.decPrivateModes.colorSchemeUpdates) { + this._reportColorScheme(); + } + })); + this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 5ba2342a..fc02715e 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -269,6 +269,26 @@ describe('InputHandler', () => { inputHandler.resetModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, false); }); + it('should toggle colorSchemeUpdates (DECSET 2031)', () => { + const coreService = new MockCoreService(); + const optionsService = new MockOptionsService(); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + // Set color scheme updates mode (default colorSchemeQuery=true) + inputHandler.setModePrivate(Params.fromArray([2031])); + assert.equal(coreService.decPrivateModes.colorSchemeUpdates, true); + // Reset color scheme updates mode + inputHandler.resetModePrivate(Params.fromArray([2031])); + assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false); + }); + it('should not toggle colorSchemeUpdates when colorSchemeQuery is disabled', () => { + const coreService = new MockCoreService(); + const optionsService = new MockOptionsService(); + optionsService.rawOptions.vtExtensions = { colorSchemeQuery: false }; + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + // Attempt to set color scheme updates mode + inputHandler.setModePrivate(Params.fromArray([2031])); + assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false); + }); }); describe('regression tests', function (): void { function termContent(bufferService: IBufferService, trim: boolean): string[] { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b2b75143..8f2ee26c 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -159,6 +159,8 @@ export class InputHandler extends Disposable implements IInputHandler { public readonly onTitleChange = this._onTitleChange.event; private readonly _onColor = this._register(new Emitter()); public readonly onColor = this._onColor.event; + private readonly _onRequestColorSchemeQuery = this._register(new Emitter()); + public readonly onRequestColorSchemeQuery = this._onRequestColorSchemeQuery.event; private _parseStack: IParseStack = { paused: false, @@ -2026,6 +2028,11 @@ export class InputHandler extends Disposable implements IInputHandler { case 2026: // synchronized output (https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md) this._coreService.decPrivateModes.synchronizedOutput = true; break; + case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) { + this._coreService.decPrivateModes.colorSchemeUpdates = true; + } + break; case 9001: // win32-input-mode (https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md) if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) { this._coreService.decPrivateModes.win32InputMode = true; @@ -2271,6 +2278,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.synchronizedOutput = false; this._onRequestRefreshRows.fire(undefined); break; + case 2031: // color scheme updates (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) { + this._coreService.decPrivateModes.colorSchemeUpdates = false; + } + break; case 9001: // win32-input-mode if (this._optionsService.rawOptions.vtExtensions?.win32InputMode) { this._coreService.decPrivateModes.win32InputMode = false; @@ -2774,6 +2786,12 @@ export class InputHandler extends Disposable implements IInputHandler { // no dec locator/mouse // this.handler(C0.ESC + '[?50n'); break; + case 996: + // color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + if (this._optionsService.rawOptions.vtExtensions?.colorSchemeQuery ?? true) { + this._onRequestColorSchemeQuery.fire(); + } + break; } return true; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 2d44c32e..363d34d4 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -106,6 +106,7 @@ export class MockCoreService implements ICoreService { applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false, + colorSchemeUpdates: false, cursorBlink: undefined, cursorStyle: undefined, origin: false, diff --git a/src/common/Types.ts b/src/common/Types.ts index 269b30c8..34c60d7f 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -268,6 +268,7 @@ export interface IDecPrivateModes { applicationCursorKeys: boolean; applicationKeypad: boolean; bracketedPasteMode: boolean; + colorSchemeUpdates: boolean; cursorBlink: boolean | undefined; cursorStyle: CursorStyle | undefined; origin: boolean; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 9981ba21..f80f25b1 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -17,6 +17,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false, + colorSchemeUpdates: false, cursorBlink: undefined, cursorStyle: undefined, origin: false, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index bbf36771..a233c693 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -313,6 +313,7 @@ export interface IVtExtensions { kittyKeyboard?: boolean; kittySgrBoldFaintControl?: boolean; win32InputMode?: boolean; + colorSchemeQuery?: boolean; } export const IOscLinkService = createDecorator('OscLinkService'); diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index a0cbc7c4..a7d1bc77 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -1234,8 +1234,30 @@ test.describe('InputHandler Integration Tests', () => { test.skip('CSI > Ps n - Disable key modifier options, xterm', () => { // TODO: Implement }); - test.describe.skip('CSI ? Ps n - DSR: Device Status Report (DEC-specific).', () => { - // TODO: Implement + test.describe('CSI ? Ps n - DECDSR: Device Status Report (DEC-specific)', () => { + test('Color Scheme Query - CSI ? 996 n (dark theme)', async () => { + // Default theme has dark background (#000000) and light foreground (#ffffff) + await ctx.proxy.write('\x1b[?996n'); + deepStrictEqual(recordedData, ['\x1b[?997;1n']); + }); + + test('Color Scheme Query - CSI ? 996 n (light theme)', async () => { + recordedData.length = 0; + await ctx.page.evaluate(`window.term.options.theme = { background: '#ffffff', foreground: '#000000' }`); + await ctx.proxy.write('\x1b[?996n'); + deepStrictEqual(recordedData, ['\x1b[?997;2n']); + // Restore default theme + await ctx.page.evaluate(`window.term.options.theme = { background: '#000000', foreground: '#ffffff' }`); + }); + + test('Color Scheme Query disabled via vtExtensions.colorSchemeQuery', async () => { + recordedData.length = 0; + await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: false }`); + await ctx.proxy.write('\x1b[?996n'); + deepStrictEqual(recordedData, []); + // Re-enable + await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: true }`); + }); }); test.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => { // TODO: Implement diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index c12d6ba9..bf06e014 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -233,7 +233,7 @@ declare module '@xterm/headless' { windowOptions?: IWindowOptions; /** - * Enable various VT extensions. All extensions are disabled by default. + * Enable various VT extensions. */ vtExtensions?: IVtExtensions; } @@ -356,6 +356,18 @@ declare module '@xterm/headless' { * [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md */ win32InputMode?: boolean; + + /** + * Whether [color scheme query and notification][0] (`CSI ? 996 n` and + * `DECSET 2031`) is enabled. When enabled, the terminal will respond to + * color scheme queries with `CSI ? 997 ; 1 n` (dark) or `CSI ? 997 ; 2 n` + * (light) based on the relative luminance of the background and foreground + * theme colors. Programs can enable unsolicited notifications via + * `CSI ? 2031 h`. The default is true. + * + * [0]: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/ + */ + colorSchemeQuery?: boolean; } /** diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ee3364dd..53362f5f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -291,7 +291,7 @@ declare module '@xterm/xterm' { theme?: ITheme; /** - * Enable various VT extensions. All extensions are disabled by default. + * Enable various VT extensions. */ vtExtensions?: IVtExtensions; @@ -473,6 +473,18 @@ declare module '@xterm/xterm' { * [0]: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md */ win32InputMode?: boolean; + + /** + * Whether [color scheme query and notification][0] (`CSI ? 996 n` and + * `DECSET 2031`) is enabled. When enabled, the terminal will respond to + * color scheme queries with `CSI ? 997 ; 1 n` (dark) or `CSI ? 997 ; 2 n` + * (light) based on the relative luminance of the background and foreground + * theme colors. Programs can enable unsolicited notifications via + * `CSI ? 2031 h`. The default is true. + * + * [0]: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/ + */ + colorSchemeQuery?: boolean; } /** From b87cba4c2afcd3685b63ad1caccbf59d2709a85e Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 28 Jan 2026 14:04:46 -0800 Subject: [PATCH 06/49] 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 07/49] 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 08/49] 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 09/49] 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 10/49] 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 11/49] 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 12/49] 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 13/49] 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 14/49] 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 15/49] 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 16/49] 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 17/49] 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 18/49] 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 19/49] 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; } From c375696ee65777fb6b0cbe97636c119c8d0783f5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:22:45 -0800 Subject: [PATCH 20/49] Make demo use strict TS compiler mode --- demo/client/client.ts | 140 +++++++++--------- .../components/window/addonImageWindow.ts | 6 +- .../components/window/addonSearchWindow.ts | 14 +- .../components/window/addonSerializeWindow.ts | 12 +- demo/client/components/window/addonsWindow.ts | 2 +- .../components/window/cellInspectorWindow.ts | 16 +- demo/client/components/window/gpuWindow.ts | 2 +- .../client/components/window/optionsWindow.ts | 32 ++-- demo/client/components/window/styleWindow.ts | 2 +- demo/client/components/window/testWindow.ts | 80 +++++----- demo/client/components/window/vtWindow.ts | 5 +- demo/client/components/window/webglWindow.ts | 2 +- demo/client/tsconfig.json | 1 + 13 files changed, 160 insertions(+), 154 deletions(-) diff --git a/demo/client/client.ts b/demo/client/client.ts index 68202ad7..45bdd1d4 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -58,11 +58,11 @@ export interface IWindowWithTerminal extends Window { } declare let window: IWindowWithTerminal; -let term; -let protocol; -let socketURL; -let socket; -let pid; +let term: Terminal | null; +let protocol: string; +let socketURL: string; +let socket: WebSocket | null; +let pid: string; let controlBar: ControlBar; let addonsWindow: AddonsWindow; let addonSearchWindow: AddonSearchWindow; @@ -73,7 +73,7 @@ const addons: AddonCollection = { attach: { name: 'attach', ctor: AttachAddon, canChange: false }, clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, - image: { name: 'image', ctor: ImageAddon, canChange: true }, + image: { name: 'image', ctor: ImageAddon!, canChange: true }, progress: { name: 'progress', ctor: ProgressAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, @@ -116,8 +116,8 @@ const xtermjsTheme = { brightWhite: '#FFFFFF' } satisfies ITheme; function setPadding(): void { - term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; - addons.fit.instance.fit(); + term!.element!.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; + addons.fit.instance!.fit(); } function getSearchOptions(): ISearchOptions { @@ -141,7 +141,7 @@ const disposeRecreateButtonHandler: () => void = () => { if (term) { term.dispose(); term = null; - window.term = null; + (window as any).term = null; socket = null; addons.attach.instance = undefined; addons.clipboard.instance = undefined; @@ -154,10 +154,10 @@ const disposeRecreateButtonHandler: () => void = () => { addons.ligatures.instance = undefined; addons.webLinks.instance = undefined; addons.webgl.instance = undefined; - document.getElementById('dispose').innerHTML = 'Recreate Terminal'; + document.getElementById('dispose')!.innerHTML = 'Recreate Terminal'; } else { createTerminal(); - document.getElementById('dispose').innerHTML = 'Dispose terminal'; + document.getElementById('dispose')!.innerHTML = 'Dispose terminal'; } }; @@ -165,7 +165,7 @@ const createNewWindowButtonHandler: () => void = () => { if (term) { disposeRecreateButtonHandler(); } - const win = window.open(); + const win = window.open()!; terminalContainer = win.document.createElement('div'); terminalContainer.id = 'terminal-container'; win.document.body.appendChild(terminalContainer); @@ -207,7 +207,7 @@ if (document.location.pathname === '/test') { } else { const typedTerm = createTerminal(); - controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []); + controlBar = new ControlBar(document.getElementById('sidebar')!, document.querySelector('.banner-tabs')!, []); optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground })); const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons)); controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons)); @@ -234,43 +234,43 @@ if (document.location.pathname === '/test') { controlBar.setTabVisible('addon-serialize', true); controlBar.setTabVisible('addon-image', true); controlBar.setTabVisible('addon-web-fonts', true); - addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e)); - addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e)); - addons.webgl.instance.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e)); + addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!); + addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e)); + addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e)); + addons.webgl.instance!.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e)); paddingElement.value = '0'; addDomListener(paddingElement, 'change', setPadding); addDomListener(actionElements.findNext, 'keydown', (e) => { if (e.key === 'Enter') { - addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions()); + addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions()); e.preventDefault(); } }); addDomListener(actionElements.findNext, 'input', (e) => { - addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions()); + addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions()); }); addDomListener(actionElements.findPrevious, 'keydown', (e) => { if (e.key === 'Enter') { - addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); + addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions()); e.preventDefault(); } }); addDomListener(actionElements.findPrevious, 'input', (e) => { - addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); + addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions()); }); addDomListener(actionElements.findNext, 'blur', (e) => { - addons.search.instance.clearActiveDecoration(); + addons.search.instance!.clearActiveDecoration(); }); addDomListener(actionElements.findPrevious, 'blur', (e) => { - addons.search.instance.clearActiveDecoration(); + addons.search.instance!.clearActiveDecoration(); }); } function createTerminal(): Terminal { // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); + while (terminalContainer!.children.length) { + terminalContainer!.removeChild(terminalContainer!.children[0]); } const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; @@ -290,7 +290,7 @@ function createTerminal(): Terminal { addons.search.instance = new SearchAddon(); addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); - addons.image.instance = new ImageAddon(); + addons.image.instance = new ImageAddon!(); addons.progress.instance = new ProgressAddon(); addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon(); addons.clipboard.instance = new ClipboardAddon(); @@ -311,7 +311,7 @@ function createTerminal(): Terminal { typedTerm.loadAddon(addons.webFonts.instance); typedTerm.loadAddon(addons.clipboard.instance); - window.term = term; // Expose `term` to window for debugging purposes + (window as any).term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { if (!pid) { return; @@ -330,7 +330,7 @@ function createTerminal(): Terminal { if (addons.webgl.instance) { try { typedTerm.loadAddon(addons.webgl.instance); - term.open(terminalContainer); + term.open(terminalContainer!); } catch (e) { console.warn('error during loading webgl addon:', e); addons.webgl.instance.dispose(); @@ -339,7 +339,7 @@ function createTerminal(): Terminal { } if (!typedTerm.element) { // webgl loading failed for some reason, attach with DOM renderer - term.open(terminalContainer); + term.open(terminalContainer!); } term.focus(); @@ -349,13 +349,13 @@ function createTerminal(): Terminal { if (optionsWindow.autoResize) { // In general this should be debounced to avoid excessive work on the main // thread by firing the expensive resize action repeatedly - addons.fit.instance.fit(); + addons.fit.instance!.fit(); } }); - resizeObserver.observe(terminalContainer); + resizeObserver.observe(terminalContainer!); window.addEventListener('resize', () => { - terminalContainer.style.width = document.body.clientWidth + 'px'; + terminalContainer!.style.width = document.body.clientWidth + 'px'; }); // fit is called within a setTimeout, cols and rows need this. @@ -369,7 +369,7 @@ function createTerminal(): Terminal { if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) { runFakeTerminal(); } else { - const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); + const res = await fetch('/terminals?cols=' + term!.cols + '&rows=' + term!.rows, { method: 'POST' }); const processId = await res.text(); pid = processId; socketURL += processId; @@ -384,52 +384,52 @@ function createTerminal(): Terminal { } function runRealTerminal(): void { - addons.attach.instance = new AttachAddon(socket); - term.loadAddon(addons.attach.instance); - term._initialized = true; - initAddons(term); + addons.attach.instance = new AttachAddon(socket!); + term!.loadAddon(addons.attach.instance); + (term as any)._initialized = true; + initAddons(term!); } function runFakeTerminal(): void { - if (term._initialized) { + if ((term as any)._initialized) { return; } - term._initialized = true; - initAddons(term); + (term as any)._initialized = true; + initAddons(term!); - term.prompt = () => { - term.write('\r\n$ '); + (term as any).prompt = () => { + term!.write('\r\n$ '); }; - term.writeln('Welcome to xterm.js'); - term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); - term.writeln('Type some keys and commands to play around.'); - term.writeln(''); - term.prompt(); + term!.writeln('Welcome to xterm.js'); + term!.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); + term!.writeln('Type some keys and commands to play around.'); + term!.writeln(''); + (term as any).prompt(); - term.onKey((e: { key: string, domEvent: KeyboardEvent }) => { + term!.onKey((e: { key: string, domEvent: KeyboardEvent }) => { const ev = e.domEvent; const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; if (ev.keyCode === 13) { - term.prompt(); + (term as any).prompt(); } else if (ev.keyCode === 8) { // Do not delete the prompt - if (term._core.buffer.x > 2) { - term.write('\b \b'); + if ((term as any)._core.buffer.x > 2) { + term!.write('\b \b'); } } else if (printable) { - term.write(e.key); + term!.write(e.key); } }); } function updateTerminalContainerBackground(): void { - if (term.options.allowTransparency) { - terminalContainer.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px'; + if (term!.options.allowTransparency) { + terminalContainer!.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px'; } else { - terminalContainer.style.background = term.options.theme?.background ?? '#000000'; + terminalContainer!.style.background = term!.options.theme?.background ?? '#000000'; } } @@ -439,19 +439,19 @@ function initAddons(term: Terminal): void { function postInitWebgl(): void { controlBar.setTabVisible('addon-webgl', true); setTimeout(() => { - addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e)); - addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e)); + addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!); + addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e)); + addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e)); }, 500); } function preDisposeWebgl(): void { controlBar.setTabVisible('addon-webgl', false); - if (addons.webgl.instance.textureAtlas) { - addons.webgl.instance.textureAtlas.remove(); + if (addons.webgl.instance!.textureAtlas) { + addons.webgl.instance!.textureAtlas.remove(); } } - Object.keys(addons).forEach((name: AddonType) => { + (Object.keys(addons) as AddonType[]).forEach(name => { const addon = addons[name]; const checkbox = document.createElement('input') as HTMLInputElement; checkbox.type = 'checkbox'; @@ -466,12 +466,12 @@ function initAddons(term: Terminal): void { term.unicode.activeVersion = '15-graphemes'; } if (name === 'search' && checkbox.checked) { - addons[name].instance.onDidChangeResults(e => updateFindResults(e)); + addons[name].instance!.onDidChangeResults(e => updateFindResults(e)); } addDomListener(checkbox, 'change', () => { if (name === 'image') { if (checkbox.checked) { - const ctorOptionsJson = document.querySelector('#image-options').value; + const ctorOptionsJson = document.querySelector('#image-options')!.value; addon.instance = ctorOptionsJson ? new addons[name].ctor(JSON.parse(ctorOptionsJson)) : new addons[name].ctor(); @@ -497,7 +497,7 @@ function initAddons(term: Terminal): void { term.unicode.activeVersion = '15-graphemes'; } else if (name === 'search') { controlBar.setTabVisible('addon-search', true); - addons[name].instance.onDidChangeResults(e => updateFindResults(e)); + addons[name].instance!.onDidChangeResults(e => updateFindResults(e)); } else if (name === 'serialize') { controlBar.setTabVisible('addon-serialize', true); } @@ -587,17 +587,17 @@ function updateFindResults(e: { resultIndex: number, resultCount: number } | und function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { element.addEventListener(type, handler); - term._core._register({ dispose: () => element.removeEventListener(type, handler) }); + (term as any)._core._register({ dispose: () => element.removeEventListener(type, handler) }); } function updateTerminalSize(): void { const width = optionsWindow.autoResize ? '100%' - : (term.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px'; + : ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px'; const height = optionsWindow.autoResize ? '100%' - : (term.dimensions.css.canvas.height).toString() + 'px'; - terminalContainer.style.width = width; - terminalContainer.style.height = height; - addons.fit.instance.fit(); + : ((term as any).dimensions.css.canvas.height).toString() + 'px'; + terminalContainer!.style.width = width; + terminalContainer!.style.height = height; + addons.fit.instance!.fit(); } (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { diff --git a/demo/client/components/window/addonImageWindow.ts b/demo/client/components/window/addonImageWindow.ts index 6c814622..87ebaff7 100644 --- a/demo/client/components/window/addonImageWindow.ts +++ b/demo/client/components/window/addonImageWindow.ts @@ -10,9 +10,9 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow { public readonly id = 'addon-image'; public readonly label = 'image'; - private _imageStorageLimitInput: HTMLInputElement; - private _imageShowPlaceholderCheckbox: HTMLInputElement; - private _imageOptionsTextarea: HTMLTextAreaElement; + private _imageStorageLimitInput!: HTMLInputElement; + private _imageShowPlaceholderCheckbox!: HTMLInputElement; + private _imageOptionsTextarea!: HTMLTextAreaElement; public build(container: HTMLElement): void { // Storage limit diff --git a/demo/client/components/window/addonSearchWindow.ts b/demo/client/components/window/addonSearchWindow.ts index bb4e15f7..2891fca9 100644 --- a/demo/client/components/window/addonSearchWindow.ts +++ b/demo/client/components/window/addonSearchWindow.ts @@ -10,13 +10,13 @@ export class AddonSearchWindow extends BaseWindow implements IControlWindow { public readonly id = 'addon-search'; public readonly label = 'search'; - private _findNextInput: HTMLInputElement; - private _findPreviousInput: HTMLInputElement; - private _findResultsSpan: HTMLElement; - private _regexCheckbox: HTMLInputElement; - private _caseSensitiveCheckbox: HTMLInputElement; - private _wholeWordCheckbox: HTMLInputElement; - private _highlightAllMatchesCheckbox: HTMLInputElement; + private _findNextInput!: HTMLInputElement; + private _findPreviousInput!: HTMLInputElement; + private _findResultsSpan!: HTMLElement; + private _regexCheckbox!: HTMLInputElement; + private _caseSensitiveCheckbox!: HTMLInputElement; + private _wholeWordCheckbox!: HTMLInputElement; + private _highlightAllMatchesCheckbox!: HTMLInputElement; public build(container: HTMLElement): void { const wrapper = document.createElement('div'); diff --git a/demo/client/components/window/addonSerializeWindow.ts b/demo/client/components/window/addonSerializeWindow.ts index ebc3df36..cb8a72ec 100644 --- a/demo/client/components/window/addonSerializeWindow.ts +++ b/demo/client/components/window/addonSerializeWindow.ts @@ -10,10 +10,10 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow { public readonly id = 'addon-serialize'; public readonly label = 'serialize'; - private _serializeOutputPre: HTMLPreElement; - private _htmlSerializeOutputPre: HTMLPreElement; - private _htmlSerializeOutputResult: HTMLElement; - private _writeToTerminalCheckbox: HTMLInputElement; + private _serializeOutputPre!: HTMLPreElement; + private _htmlSerializeOutputPre!: HTMLPreElement; + private _htmlSerializeOutputResult!: HTMLElement; + private _writeToTerminalCheckbox!: HTMLInputElement; public build(container: HTMLElement): void { const wrapper = document.createElement('div'); @@ -64,7 +64,7 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow { } private _serializeButtonHandler(): void { - const output = this._addons.serialize.instance.serialize(); + const output = this._addons.serialize.instance!.serialize(); const outputString = JSON.stringify(output); this._serializeOutputPre.innerText = outputString; @@ -75,7 +75,7 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow { } private _htmlSerializeButtonHandler(): void { - const output = this._addons.serialize.instance.serializeAsHTML(); + const output = this._addons.serialize.instance!.serializeAsHTML(); this._htmlSerializeOutputPre.innerText = output; // Deprecated, but the most supported for now. diff --git a/demo/client/components/window/addonsWindow.ts b/demo/client/components/window/addonsWindow.ts index c69bf521..8dc31a07 100644 --- a/demo/client/components/window/addonsWindow.ts +++ b/demo/client/components/window/addonsWindow.ts @@ -10,7 +10,7 @@ export class AddonsWindow extends BaseWindow implements IControlWindow { public readonly id = 'addons'; public readonly label = 'Addons'; - private _addonsContainer: HTMLElement; + private _addonsContainer!: HTMLElement; public build(container: HTMLElement): void { // Description diff --git a/demo/client/components/window/cellInspectorWindow.ts b/demo/client/components/window/cellInspectorWindow.ts index 9098fc5e..b7a50bf6 100644 --- a/demo/client/components/window/cellInspectorWindow.ts +++ b/demo/client/components/window/cellInspectorWindow.ts @@ -30,14 +30,14 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow { public readonly id = 'cell-inspector'; public readonly label = 'Cell Inspector'; - private _container: HTMLElement; - private _positionEl: HTMLElement; - private _charEl: HTMLElement; - private _codeEl: HTMLElement; - private _widthEl: HTMLElement; - private _fgEl: HTMLElement; - private _bgEl: HTMLElement; - private _attrsEl: HTMLElement; + private _container!: HTMLElement; + private _positionEl!: HTMLElement; + private _charEl!: HTMLElement; + private _codeEl!: HTMLElement; + private _widthEl!: HTMLElement; + private _fgEl!: HTMLElement; + private _bgEl!: HTMLElement; + private _attrsEl!: HTMLElement; public build(container: HTMLElement): void { this._container = container; diff --git a/demo/client/components/window/gpuWindow.ts b/demo/client/components/window/gpuWindow.ts index 84bccf11..ac7b2c3d 100644 --- a/demo/client/components/window/gpuWindow.ts +++ b/demo/client/components/window/gpuWindow.ts @@ -10,7 +10,7 @@ export class GpuWindow extends BaseWindow implements IControlWindow { public readonly id = 'gpu'; public readonly label = 'WebGL'; - private _textureAtlasContainer: HTMLElement; + private _textureAtlasContainer!: HTMLElement; public build(container: HTMLElement): void { const zoomCheckbox = document.createElement('input'); diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts index 7f8c4e2f..1071cfd5 100644 --- a/demo/client/components/window/optionsWindow.ts +++ b/demo/client/components/window/optionsWindow.ts @@ -80,8 +80,8 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { public readonly id = 'options'; public readonly label = 'Options'; - private _container: HTMLElement; - private _optionsContainer: HTMLElement; + private _container!: HTMLElement; + private _optionsContainer!: HTMLElement; private _autoResize: boolean = true; constructor( @@ -143,7 +143,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { const booleanOptions: string[] = []; const numberOptions: string[] = []; options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { - switch (typeof this._terminal.options[o]) { + switch (typeof (this._terminal.options as Record)[o]) { case 'boolean': booleanOptions.push(o); break; @@ -160,25 +160,25 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { let html = ''; html += '
'; booleanOptions.forEach(o => { - html += `
`; + html += `
`; }); nestedBooleanOptions.forEach(({ label, parent, prop }) => { - const checked = this._terminal.options[parent]?.[prop] ?? false; + const checked = (this._terminal.options as Record | undefined>)[parent]?.[prop] ?? false; html += `
`; }); html += '
'; numberOptions.forEach(o => { - html += `
`; + html += `
`; }); html += '
'; Object.keys(stringOptions).forEach(o => { if (o === 'colsRows') { html += `
`; } else if (stringOptions[o]) { - const selectedOption = o === 'theme' ? 'xtermjs' : this._terminal.options[o]; + const selectedOption = o === 'theme' ? 'xtermjs' : (this._terminal.options as Record)[o]; html += `
`; } else { - html += `
`; + html += `
`; } }); html += '
'; @@ -190,7 +190,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.checked); - this._terminal.options[o] = input.checked; + (this._terminal.options as Record)[o] = input.checked; if (o ==='allowTransparency') { this._terminal.options.theme = this._getTheme(); this._handlers.updateTerminalContainerBackground(); @@ -201,7 +201,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', label, input.checked); - this._terminal.options[parent] = { ...this._terminal.options[parent], [prop]: input.checked }; + (this._terminal.options as Record)[parent] = { ...(this._terminal.options as Record | undefined>)[parent], [prop]: input.checked }; }); }); numberOptions.forEach(o => { @@ -216,7 +216,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { this._terminal.options.scrollback = parseInt(input.value); setTimeout(() => this._handlers.updateTerminalSize(), 5); } else { - this._terminal.options[o] = parseInt(input.value); + (this._terminal.options as Record)[o] = parseInt(input.value); } this._handlers.updateTerminalSize(); }); @@ -225,7 +225,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.value); - let value: any = input.value; + let value: unknown = input.value; if (o === 'colsRows') { const m = input.value.match(/^([0-9]+)x([0-9]+)$/); if (m) { @@ -239,7 +239,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { } else if (o === 'theme') { value = this._getTheme(); } - this._terminal.options[o] = value; + (this._terminal.options as Record)[o] = value; if (o === 'theme') { this._handlers.updateTerminalContainerBackground(); } @@ -259,11 +259,11 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { } private _getTheme(): ITheme { - const input = document.querySelector('#opt-theme'); - let theme: ITheme; + const input = document.querySelector('#opt-theme')!; + let theme: ITheme = {}; switch (input.value) { case 'default': - theme = undefined; + theme = {}; break; case 'xtermjs': theme = { ...xtermjsTheme }; diff --git a/demo/client/components/window/styleWindow.ts b/demo/client/components/window/styleWindow.ts index 94cf6e4c..0a6fbed5 100644 --- a/demo/client/components/window/styleWindow.ts +++ b/demo/client/components/window/styleWindow.ts @@ -10,7 +10,7 @@ export class StyleWindow extends BaseWindow implements IControlWindow { public readonly id = 'style'; public readonly label = 'Style'; - private _paddingElement: HTMLInputElement; + private _paddingElement!: HTMLInputElement; public build(container: HTMLElement): void { const wrapper = document.createElement('div'); diff --git a/demo/client/components/window/testWindow.ts b/demo/client/components/window/testWindow.ts index 12c6e995..e8e9ed66 100644 --- a/demo/client/components/window/testWindow.ts +++ b/demo/client/components/window/testWindow.ts @@ -139,7 +139,9 @@ export class TestWindow extends BaseWindow implements IControlWindow { } dd.appendChild(button); dl.appendChild(dd); - button.addEventListener('click', handler); + if (handler) { + button.addEventListener('click', handler); + } } private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void { @@ -465,7 +467,7 @@ function customGlyphAlignmentHandler(term: Terminal): void { while (fillChars.length > 0) { const batch = fillChars.splice(0, 10); for (const fillChar of batch) { - term.write(`${fillChar.codePointAt(0).toString(16).toUpperCase().padEnd(5, ' ')} `); + term.write(`${fillChar.codePointAt(0)!.toString(16).toUpperCase().padEnd(5, ' ')} `); } term.write('\n\r'); for (let i = 0; i < 3; i++) { @@ -864,7 +866,7 @@ function addDecoration(term: Terminal, dim: number = 1): void { foregroundColor: '#00FE00', overviewRulerOptions: { color: '#ef292980', position: 'left' } }); - decoration.onRender((e: HTMLElement) => { + decoration?.onRender((e: HTMLElement) => { e.style.right = '100%'; e.style.backgroundColor = '#ef292980'; }); @@ -896,13 +898,16 @@ function decorationStressTest(term: Terminal): void { for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) { for (let y = 0; y < term.buffer.active.length; y++) { const cursorOffsetY = y - cursorY; - decorationStressTestDecorations.push(term.registerDecoration({ + const decoration = term.registerDecoration({ marker: term.registerMarker(cursorOffsetY), x, width: 4, backgroundColor: '#FF0000', overviewRulerOptions: { color: '#FF0000' } - })); + }); + if (decoration) { + decorationStressTestDecorations.push(decoration); + } } } } @@ -919,56 +924,57 @@ function initProgress(term: Terminal, addons: AddonCollection): void { // NOTE: This is most likely not what you want to do for other progress indicators, // that have a proper visual state for error/paused. value = Math.min(10 + value * 0.9, 100); - document.getElementById('progress-percent').style.width = `${value}%`; - document.getElementById('progress-percent').style.backgroundColor = COLORS[state]; - document.getElementById('progress-state').innerText = `State: ${STATES[state]}`; + document.getElementById('progress-percent')!.style.width = `${value}%`; + document.getElementById('progress-percent')!.style.backgroundColor = COLORS[state]; + document.getElementById('progress-state')!.innerText = `State: ${STATES[state]}`; - document.getElementById('progress-percent').style.display = state === 3 ? 'none' : 'block'; - document.getElementById('progress-indeterminate').style.display = state === 3 ? 'block' : 'none'; + document.getElementById('progress-percent')!.style.display = state === 3 ? 'none' : 'block'; + document.getElementById('progress-indeterminate')!.style.display = state === 3 ? 'block' : 'none'; } - const progressAddon = addons.progress.instance; + const progressAddon = addons.progress.instance!; progressAddon.onChange(progressHandler); // apply initial state once to make it visible on page load const initialProgress = progressAddon.progress; progressHandler(initialProgress); - document.getElementById('progress-run').addEventListener('click', async () => { + document.getElementById('progress-run')!.addEventListener('click', async () => { term.write('\x1b]9;4;0\x1b\\'); for (let i = 0; i <= 100; i += 5) { term.write(`\x1b]9;4;1;${i}\x1b\\`); await new Promise(res => setTimeout(res, 200)); } }); - document.getElementById('progress-0').addEventListener('click', () => term.write('\x1b]9;4;0\x1b\\')); - document.getElementById('progress-1').addEventListener('click', () => term.write('\x1b]9;4;1;20\x1b\\')); - document.getElementById('progress-2').addEventListener('click', () => term.write('\x1b]9;4;2\x1b\\')); - document.getElementById('progress-3').addEventListener('click', () => term.write('\x1b]9;4;3\x1b\\')); - document.getElementById('progress-4').addEventListener('click', () => term.write('\x1b]9;4;4\x1b\\')); + document.getElementById('progress-0')!.addEventListener('click', () => term.write('\x1b]9;4;0\x1b\\')); + document.getElementById('progress-1')!.addEventListener('click', () => term.write('\x1b]9;4;1;20\x1b\\')); + document.getElementById('progress-2')!.addEventListener('click', () => term.write('\x1b]9;4;2\x1b\\')); + document.getElementById('progress-3')!.addEventListener('click', () => term.write('\x1b]9;4;3\x1b\\')); + document.getElementById('progress-4')!.addEventListener('click', () => term.write('\x1b]9;4;4\x1b\\')); } function initImageAddonExposed(term: Terminal, addons: AddonCollection): void { - const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts; - const limitStorageElement = document.querySelector('#image-storagelimit'); - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; + const imageAddon = addons.image.instance!; + const DEFAULT_OPTIONS: IImageAddonOptions = (imageAddon as any)._defaultOpts; + const limitStorageElement = document.querySelector('#image-storagelimit')!; + limitStorageElement.valueAsNumber = imageAddon.storageLimit; addDomListener(term, limitStorageElement, 'change', () => { try { - addons.image.instance.storageLimit = limitStorageElement.valueAsNumber; - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - console.log('changed storageLimit to', addons.image.instance.storageLimit); + imageAddon.storageLimit = limitStorageElement.valueAsNumber; + limitStorageElement.valueAsNumber = imageAddon.storageLimit; + console.log('changed storageLimit to', imageAddon.storageLimit); } catch (e) { - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - console.log('storageLimit at', addons.image.instance.storageLimit); + limitStorageElement.valueAsNumber = imageAddon.storageLimit; + console.log('storageLimit at', imageAddon.storageLimit); throw e; } }); - const showPlaceholderElement = document.querySelector('#image-showplaceholder'); - showPlaceholderElement.checked = addons.image.instance.showPlaceholder; + const showPlaceholderElement = document.querySelector('#image-showplaceholder')!; + showPlaceholderElement.checked = imageAddon.showPlaceholder; addDomListener(term, showPlaceholderElement, 'change', () => { - addons.image.instance.showPlaceholder = showPlaceholderElement.checked; + imageAddon.showPlaceholder = showPlaceholderElement.checked; }); - const ctorOptionsElement = document.querySelector('#image-options'); + const ctorOptionsElement = document.querySelector('#image-options')!; ctorOptionsElement.value = JSON.stringify(DEFAULT_OPTIONS, null, 2); const sixelDemo = (url: string) => () => fetch(url) @@ -988,16 +994,16 @@ function initImageAddonExposed(term: Terminal, addons: AddonCollection): void { term.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`); }); - document.getElementById('image-demo1').addEventListener('click', + document.getElementById('image-demo1')!.addEventListener('click', sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six')); - document.getElementById('image-demo2').addEventListener('click', + document.getElementById('image-demo2')!.addEventListener('click', sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel')); - document.getElementById('image-demo3').addEventListener('click', + document.getElementById('image-demo3')!.addEventListener('click', iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png')); // demo for image retrieval API - term.element.addEventListener('click', (ev: MouseEvent) => { - if (!ev.ctrlKey || !addons.image.instance) return; + term.element!.addEventListener('click', (ev: MouseEvent) => { + if (!ev.ctrlKey || !imageAddon) return; // TODO... // if (ev.altKey) { @@ -1015,10 +1021,10 @@ function initImageAddonExposed(term: Terminal, addons: AddonCollection): void { const y = pos[1] - 1; const canvas = ev.shiftKey // ctrl+shift+click: get single tile - ? addons.image.instance.extractTileAtBufferCell(x, term.buffer.active.viewportY + y) + ? imageAddon.extractTileAtBufferCell(x, term.buffer.active.viewportY + y) // ctrl+click: get original image - : addons.image.instance.getImageAtBufferCell(x, term.buffer.active.viewportY + y); - canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); + : imageAddon.getImageAtBufferCell(x, term.buffer.active.viewportY + y); + canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank')); }); } diff --git a/demo/client/components/window/vtWindow.ts b/demo/client/components/window/vtWindow.ts index b5d01367..08f481b1 100644 --- a/demo/client/components/window/vtWindow.ts +++ b/demo/client/components/window/vtWindow.ts @@ -11,8 +11,7 @@ export class VtWindow extends BaseWindow implements IControlWindow { public readonly id = 'vt'; public readonly label = 'VT'; - private _container: HTMLElement; - private _term: Terminal | undefined; + private _container!: HTMLElement; public build(container: HTMLElement): void { this._container = container; @@ -65,7 +64,7 @@ export class VtWindow extends BaseWindow implements IControlWindow { const writeCsiSplit = writeCsi.split('|'); const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : ''; const suffix = writeCsiSplit[writeCsiSplit.length - 1]; - element.addEventListener('click', () => this._term?.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); + element.addEventListener('click', () => this._terminal.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); const desc = document.createElement('span'); desc.textContent = description; diff --git a/demo/client/components/window/webglWindow.ts b/demo/client/components/window/webglWindow.ts index f4fabf63..b61be231 100644 --- a/demo/client/components/window/webglWindow.ts +++ b/demo/client/components/window/webglWindow.ts @@ -10,7 +10,7 @@ export class WebglWindow extends BaseWindow implements IControlWindow { public readonly id = 'addon-webgl'; public readonly label = 'webgl'; - private _textureAtlasContainer: HTMLElement; + private _textureAtlasContainer!: HTMLElement; public build(container: HTMLElement): void { const zoomCheckbox = document.createElement('input'); diff --git a/demo/client/tsconfig.json b/demo/client/tsconfig.json index 17aae876..6c601531 100644 --- a/demo/client/tsconfig.json +++ b/demo/client/tsconfig.json @@ -6,6 +6,7 @@ "rootDir": ".", "sourceMap": true, "baseUrl": ".", + "strict": true, "paths": { "@xterm/addon-attach": ["../../addons/addon-attach"], "@xterm/addon-clipboard": ["../../addons/addon-clipboard"], From 6c3d4e4ea423f5041fa962e29c4804d1cc0e294f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:26:05 -0800 Subject: [PATCH 21/49] Fix lint --- demo/client/components/window/vtWindow.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/client/components/window/vtWindow.ts b/demo/client/components/window/vtWindow.ts index 08f481b1..60c34101 100644 --- a/demo/client/components/window/vtWindow.ts +++ b/demo/client/components/window/vtWindow.ts @@ -5,7 +5,6 @@ import { BaseWindow } from './baseWindow'; import type { IControlWindow } from '../controlBar'; -import type { Terminal } from '@xterm/xterm'; export class VtWindow extends BaseWindow implements IControlWindow { public readonly id = 'vt'; From e542affaca41480f4656f785d162a4a90b888051 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:17:52 -0800 Subject: [PATCH 22/49] Use native padStart over custom impl --- addons/addon-serialize/src/SerializeAddon.ts | 18 ++---------------- .../renderer/dom/DomRendererRowFactory.ts | 11 ++--------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index deb3e2cc..fadeb5bb 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -630,20 +630,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { } } - private _padStart(target: string, targetLength: number, padString: string): string { - targetLength = targetLength >> 0; - padString = padString ?? ' '; - if (target.length > targetLength) { - return target; - } - - targetLength -= target.length; - if (targetLength > padString.length) { - padString += padString.repeat(targetLength / padString.length); - } - return padString.slice(0, targetLength) + target; - } - protected _beforeSerialize(rows: number, start: number, end: number): void { this._htmlContent += '
';
 
@@ -680,7 +666,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
         (color >>  8) & 255,
         (color      ) & 255
       ];
-      return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
+      return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
     }
     if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
       return this._ansiColors[color].css;
@@ -700,7 +686,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
         (color >>  8) & 255,
         (color      ) & 255
       ];
-      return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
+      return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
     }
     // Palette color
     return this._ansiColors[color].css;
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts
index e95d221b..e69c317b 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -397,7 +397,7 @@ export class DomRendererRowFactory {
           break;
         case Attributes.CM_RGB:
           resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
-          this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
+          this._addStyle(charElement, `background-color:#${(bg >>> 0).toString(16).padStart(6, '0')}`);
           break;
         case Attributes.CM_DEFAULT:
         default:
@@ -434,7 +434,7 @@ export class DomRendererRowFactory {
             (fg      ) & 0xFF
           );
           if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
-            this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
+            this._addStyle(charElement, `color:#${fg.toString(16).padStart(6, '0')}`);
           }
           break;
         case Attributes.CM_DEFAULT:
@@ -537,10 +537,3 @@ export class DomRendererRowFactory {
         (start[1] < end[1] && y === start[1] && x >= start[0]);
   }
 }
-
-function padStart(text: string, padChar: string, length: number): string {
-  while (text.length < length) {
-    text = padChar + text;
-  }
-  return text;
-}

From 39037ee806077535e69e6f396536dc9eb9c15a9c Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 10:20:44 -0800
Subject: [PATCH 23/49] Prefer startsWith over indexOf === 0

Faster and make code clearer
---
 src/common/InputHandler.ts      | 2 +-
 src/common/input/XParseColor.ts | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index 1bb923e6..eb83cf8a 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -1780,7 +1780,7 @@ export class InputHandler extends Disposable implements IInputHandler {
    * @param term The terminal name to evaluate
    */
   private _is(term: string): boolean {
-    return (this._optionsService.rawOptions.termName + '').indexOf(term) === 0;
+    return (this._optionsService.rawOptions.termName + '').startsWith(term);
   }
 
   /**
diff --git a/src/common/input/XParseColor.ts b/src/common/input/XParseColor.ts
index fd23ec4b..ae4a3bca 100644
--- a/src/common/input/XParseColor.ts
+++ b/src/common/input/XParseColor.ts
@@ -24,7 +24,7 @@ export function parseColor(data: string): [number, number, number] | undefined {
   if (!data) return;
   // also handle uppercases
   let low = data.toLowerCase();
-  if (low.indexOf('rgb:') === 0) {
+  if (low.startsWith('rgb:')) {
     // 'rgb:' specifier
     low = low.slice(4);
     const m = RGB_REX.exec(low);
@@ -36,7 +36,7 @@ export function parseColor(data: string): [number, number, number] | undefined {
         Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)
       ];
     }
-  } else if (low.indexOf('#') === 0) {
+  } else if (low.startsWith('#')) {
     // '#' specifier
     low = low.slice(1);
     if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {

From 3ef43aedb114e0682ebdc2ea0fe1059ebd4fdda5 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 10:26:15 -0800
Subject: [PATCH 24/49] Cache work cell in hot loops

---
 src/browser/OscLinkProvider.ts  | 4 +++-
 src/common/buffer/BufferLine.ts | 7 +++----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts
index 18b0d2ba..b6a3cce0 100644
--- a/src/browser/OscLinkProvider.ts
+++ b/src/browser/OscLinkProvider.ts
@@ -9,6 +9,8 @@ import { CellData } from 'common/buffer/CellData';
 import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
 
 export class OscLinkProvider implements ILinkProvider {
+  private readonly _workCell = new CellData();
+
   constructor(
     @IBufferService private readonly _bufferService: IBufferService,
     @IOptionsService private readonly _optionsService: IOptionsService,
@@ -25,7 +27,7 @@ export class OscLinkProvider implements ILinkProvider {
 
     const result: ILink[] = [];
     const linkHandler = this._optionsService.rawOptions.linkHandler;
-    const cell = new CellData();
+    const cell = this._workCell;
     const lineLength = line.getTrimmedLength();
     let currentLinkId = -1;
     let currentStart = -1;
diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts
index 03177076..e415851a 100644
--- a/src/common/buffer/BufferLine.ts
+++ b/src/common/buffer/BufferLine.ts
@@ -39,6 +39,7 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
 
 // Work variables to avoid garbage collection
 let $startIndex = 0;
+const $workCell = new CellData();
 
 /** Factor when to cleanup underlying array buffer after shrinking. */
 const CLEANUP_THRESHOLD = 2;
@@ -262,9 +263,8 @@ export class BufferLine implements IBufferLine {
     }
 
     if (n < this.length - pos) {
-      const cell = new CellData();
       for (let i = this.length - pos - n - 1; i >= 0; --i) {
-        this.setCell(pos + n + i, this.loadCell(pos + i, cell));
+        this.setCell(pos + n + i, this.loadCell(pos + i, $workCell));
       }
       for (let i = 0; i < n; ++i) {
         this.setCell(pos + i, fillCellData);
@@ -284,9 +284,8 @@ export class BufferLine implements IBufferLine {
   public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
     pos %= this.length;
     if (n < this.length - pos) {
-      const cell = new CellData();
       for (let i = 0; i < this.length - pos - n; ++i) {
-        this.setCell(pos + i, this.loadCell(pos + n + i, cell));
+        this.setCell(pos + i, this.loadCell(pos + n + i, $workCell));
       }
       for (let i = this.length - n; i < this.length; ++i) {
         this.setCell(i, fillCellData);

From 042bb4bed922d71a037ed0cb15f0f56a067d67af Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 14:42:23 -0800
Subject: [PATCH 25/49] Optimize Emitter.fire for 0 and 1 listeners

Before:
      Context "Emitter.fire()"
         Context "0 listeners"
            Case "#1" : 1 runs - average throughput: 97.77 MB/s
         Context "1 listener"
            Case "#1" : 1 runs - average throughput: 49.30 MB/s
         Context "2 listeners"
            Case "#1" : 1 runs - average throughput: 35.37 MB/s
         Context "5 listeners"
            Case "#1" : 1 runs - average throughput: 15.32 MB/s

After:
      Context "Emitter.fire()"
         Context "0 listeners"
            Case "#1" : 1 runs - average throughput: 376.94 MB/s
         Context "1 listener"
            Case "#1" : 1 runs - average throughput: 84.29 MB/s
         Context "2 listeners"
            Case "#1" : 1 runs - average throughput: 32.36 MB/s
         Context "5 listeners"
            Case "#1" : 1 runs - average throughput: 15.93 MB/s
---
 src/common/Event.test.ts          | 72 +++++++++++++++++++++++++++++++
 src/common/Event.ts               | 18 ++++++--
 test/benchmark/Event.benchmark.ts | 72 +++++++++++++++++++++++++++++++
 3 files changed, 158 insertions(+), 4 deletions(-)
 create mode 100644 src/common/Event.test.ts
 create mode 100644 test/benchmark/Event.benchmark.ts

diff --git a/src/common/Event.test.ts b/src/common/Event.test.ts
new file mode 100644
index 00000000..8d1cfadd
--- /dev/null
+++ b/src/common/Event.test.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert } from 'chai';
+import { Emitter } from 'common/Event';
+
+describe('Emitter', () => {
+  it('should fire with 0 listeners without error', () => {
+    const emitter = new Emitter();
+    emitter.fire(42);
+  });
+
+  it('should fire with 1 listener', () => {
+    const emitter = new Emitter();
+    let received: number | undefined;
+    emitter.event(e => { received = e; });
+    emitter.fire(42);
+    assert.strictEqual(received, 42);
+  });
+
+  it('should fire with 1 listener using thisArgs', () => {
+    const emitter = new Emitter();
+    const obj = { value: 0, handler(e: number) { this.value = e; } };
+    emitter.event(obj.handler, obj);
+    emitter.fire(42);
+    assert.strictEqual(obj.value, 42);
+  });
+
+  it('should fire with multiple listeners', () => {
+    const emitter = new Emitter();
+    const results: number[] = [];
+    emitter.event(e => results.push(e * 1));
+    emitter.event(e => results.push(e * 2));
+    emitter.event(e => results.push(e * 3));
+    emitter.fire(10);
+    assert.deepEqual(results, [10, 20, 30]);
+  });
+
+  it('should handle listener removal during fire', () => {
+    const emitter = new Emitter();
+    const results: string[] = [];
+    emitter.event(() => results.push('first'));
+    const disposable = emitter.event(() => {
+      results.push('second');
+      disposable.dispose();
+    });
+    emitter.event(() => results.push('third'));
+    emitter.fire(1);
+    assert.deepEqual(results, ['first', 'second', 'third']);
+  });
+
+  it('should not fire after dispose', () => {
+    const emitter = new Emitter();
+    let called = false;
+    emitter.event(() => { called = true; });
+    emitter.dispose();
+    emitter.fire(42);
+    assert.strictEqual(called, false);
+  });
+
+  it('should allow disposing a listener', () => {
+    const emitter = new Emitter();
+    let count = 0;
+    const disposable = emitter.event(() => { count++; });
+    emitter.fire(1);
+    disposable.dispose();
+    emitter.fire(2);
+    assert.strictEqual(count, 1);
+  });
+});
diff --git a/src/common/Event.ts b/src/common/Event.ts
index f9fcb4e7..7173a955 100644
--- a/src/common/Event.ts
+++ b/src/common/Event.ts
@@ -53,10 +53,20 @@ export class Emitter {
     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);
+    switch (this._listeners.length) {
+      case 0: return;
+      case 1: {
+        const { fn, thisArgs } = this._listeners[0];
+        fn.call(thisArgs, event);
+        return;
+      }
+      default: {
+        // Snapshot listeners to allow modifications during iteration (2+ listeners)
+        const listeners = this._listeners.slice();
+        for (const { fn, thisArgs } of listeners) {
+          fn.call(thisArgs, event);
+        }
+      }
     }
   }
 
diff --git a/test/benchmark/Event.benchmark.ts b/test/benchmark/Event.benchmark.ts
new file mode 100644
index 00000000..76f23f91
--- /dev/null
+++ b/test/benchmark/Event.benchmark.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
+import { Emitter } from 'common/Event';
+
+const ITERATIONS = 1_000_000;
+
+perfContext('Emitter.fire()', () => {
+  perfContext('0 listeners', () => {
+    let emitter: Emitter;
+    before(() => {
+      emitter = new Emitter();
+    });
+    new ThroughputRuntimeCase('', () => {
+      for (let i = 0; i < ITERATIONS; i++) {
+        emitter.fire(i);
+      }
+      return { payloadSize: ITERATIONS };
+    }, { fork: false }).showAverageThroughput();
+  });
+
+  perfContext('1 listener', () => {
+    let emitter: Emitter;
+    let sum = 0;
+    before(() => {
+      emitter = new Emitter();
+      emitter.event(e => { sum += e; });
+    });
+    new ThroughputRuntimeCase('', () => {
+      for (let i = 0; i < ITERATIONS; i++) {
+        emitter.fire(i);
+      }
+      return { payloadSize: ITERATIONS };
+    }, { fork: false }).showAverageThroughput();
+  });
+
+  perfContext('2 listeners', () => {
+    let emitter: Emitter;
+    let sum = 0;
+    before(() => {
+      emitter = new Emitter();
+      emitter.event(e => { sum += e; });
+      emitter.event(e => { sum += e * 2; });
+    });
+    new ThroughputRuntimeCase('', () => {
+      for (let i = 0; i < ITERATIONS; i++) {
+        emitter.fire(i);
+      }
+      return { payloadSize: ITERATIONS };
+    }, { fork: false }).showAverageThroughput();
+  });
+
+  perfContext('5 listeners', () => {
+    let emitter: Emitter;
+    let sum = 0;
+    before(() => {
+      emitter = new Emitter();
+      for (let j = 0; j < 5; j++) {
+        emitter.event(e => { sum += e; });
+      }
+    });
+    new ThroughputRuntimeCase('', () => {
+      for (let i = 0; i < ITERATIONS; i++) {
+        emitter.fire(i);
+      }
+      return { payloadSize: ITERATIONS };
+    }, { fork: false }).showAverageThroughput();
+  });
+});

From 5ecdc7da446120708c491e08253775281bdfab5d Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:08:05 -0800
Subject: [PATCH 26/49] Make payload limit configurable by tests

---
 src/common/parser/ApcParser.ts | 4 +++-
 src/common/parser/DcsParser.ts | 4 +++-
 src/common/parser/OscParser.ts | 4 +++-
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/common/parser/ApcParser.ts b/src/common/parser/ApcParser.ts
index e5a82ce3..eff533e2 100644
--- a/src/common/parser/ApcParser.ts
+++ b/src/common/parser/ApcParser.ts
@@ -199,6 +199,8 @@ export class ApcParser implements IApcParser {
  * as APC handlers.
  */
 export class ApcHandler implements IApcHandler {
+  private static PAYLOAD_LIMIT = PAYLOAD_LIMIT;
+
   private _data = '';
   private _hitLimit: boolean = false;
 
@@ -214,7 +216,7 @@ export class ApcHandler implements IApcHandler {
       return;
     }
     this._data += utf32ToString(data, start, end);
-    if (this._data.length > PAYLOAD_LIMIT) {
+    if (this._data.length > ApcHandler.PAYLOAD_LIMIT) {
       this._data = '';
       this._hitLimit = true;
     }
diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts
index 182179b0..50253997 100644
--- a/src/common/parser/DcsParser.ts
+++ b/src/common/parser/DcsParser.ts
@@ -138,6 +138,8 @@ EMPTY_PARAMS.addParam(0);
  * Note: The payload is currently limited to 50 MB (hardcoded).
  */
 export class DcsHandler implements IDcsHandler {
+  private static PAYLOAD_LIMIT = PAYLOAD_LIMIT;
+
   private _data = '';
   private _params: IParams = EMPTY_PARAMS;
   private _hitLimit: boolean = false;
@@ -159,7 +161,7 @@ export class DcsHandler implements IDcsHandler {
       return;
     }
     this._data += utf32ToString(data, start, end);
-    if (this._data.length > PAYLOAD_LIMIT) {
+    if (this._data.length > DcsHandler.PAYLOAD_LIMIT) {
       this._data = '';
       this._hitLimit = true;
     }
diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts
index 2a829a8e..cea9e12c 100644
--- a/src/common/parser/OscParser.ts
+++ b/src/common/parser/OscParser.ts
@@ -192,6 +192,8 @@ export class OscParser implements IOscParser {
  * as OSC handlers.
  */
 export class OscHandler implements IOscHandler {
+  private static PAYLOAD_LIMIT = PAYLOAD_LIMIT;
+
   private _data = '';
   private _hitLimit: boolean = false;
 
@@ -207,7 +209,7 @@ export class OscHandler implements IOscHandler {
       return;
     }
     this._data += utf32ToString(data, start, end);
-    if (this._data.length > PAYLOAD_LIMIT) {
+    if (this._data.length > OscHandler.PAYLOAD_LIMIT) {
       this._data = '';
       this._hitLimit = true;
     }

From 7ceb15bb83273d5cb2f698b359cf540dd3b7a121 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:17:10 -0800
Subject: [PATCH 27/49] Reduce payload size to speed up unit tests

Reduces 11s -> 8s for full run
---
 package.json                        |  1 +
 src/common/parser/ApcParser.test.ts | 26 ++++++++++++++++++++------
 src/common/parser/DcsParser.test.ts | 26 ++++++++++++++++++++------
 src/common/parser/OscParser.test.ts | 26 ++++++++++++++++++++------
 4 files changed, 61 insertions(+), 18 deletions(-)

diff --git a/package.json b/package.json
index c319f0b3..bc66fbd4 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
     "lint-fix": "eslint --fix src/ addons/ demo/",
     "lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/",
     "test-unit": "node ./bin/test_unit.js",
+    "test-unit-slow-tests": "npm run test-unit | grep \"ms)\"",
     "test-unit-coverage": "node ./bin/test_unit.js --coverage",
     "test-unit-dev": "cross-env NODE_PATH='./out' mocha",
     "test-integration": "node ./bin/test_integration.js --workers=75%",
diff --git a/src/common/parser/ApcParser.test.ts b/src/common/parser/ApcParser.test.ts
index 3401baab..f337e5c0 100644
--- a/src/common/parser/ApcParser.test.ts
+++ b/src/common/parser/ApcParser.test.ts
@@ -6,7 +6,6 @@ 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);
@@ -216,6 +215,21 @@ describe('ApcParser', () => {
   });
 
   describe('ApcHandler convenience class', () => {
+    const TEST_PAYLOAD_LIMIT = 100;
+    const CHUNK_SIZE = 10;
+    let originalPayloadLimit: number;
+
+    beforeEach(() => {
+      const handlerConstructor = ApcHandler as unknown as { PAYLOAD_LIMIT: number };
+      originalPayloadLimit = handlerConstructor.PAYLOAD_LIMIT;
+      handlerConstructor.PAYLOAD_LIMIT = TEST_PAYLOAD_LIMIT;
+    });
+
+    afterEach(() => {
+      const handlerConstructor = ApcHandler as unknown as { PAYLOAD_LIMIT: number };
+      handlerConstructor.PAYLOAD_LIMIT = originalPayloadLimit;
+    });
+
     it('should be called once on end(true)', () => {
       const G_CODE = 0x47;
       const results: [number, string][] = [];
@@ -257,12 +271,12 @@ describe('ApcParser', () => {
       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) {
+      data = toUtf32('A'.repeat(CHUNK_SIZE));
+      for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
         parser.put(data, 0, data.length);
       }
       parser.end(true);
-      assert.deepEqual(results, [[G_CODE, 'A'.repeat(PAYLOAD_LIMIT)]]);
+      assert.deepEqual(results, [[G_CODE, 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
     });
 
     it('should abort for payload over limit', function(): void {
@@ -276,8 +290,8 @@ describe('ApcParser', () => {
       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) {
+      data = toUtf32('A'.repeat(CHUNK_SIZE));
+      for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
         parser.put(data, 0, data.length);
       }
       data = toUtf32('A');
diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts
index c6cc0994..0c09199a 100644
--- a/src/common/parser/DcsParser.test.ts
+++ b/src/common/parser/DcsParser.test.ts
@@ -7,7 +7,6 @@ import { DcsParser, DcsHandler } from 'common/parser/DcsParser';
 import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types';
 import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder';
 import { Params } from 'common/parser/Params';
-import { PAYLOAD_LIMIT } from 'common/parser/Constants';
 
 function toUtf32(s: string): Uint32Array {
   const utf32 = new Uint32Array(s.length);
@@ -176,6 +175,21 @@ describe('DcsParser', () => {
     });
   });
   describe('DcsHandlerFactory', () => {
+    const TEST_PAYLOAD_LIMIT = 100;
+    const CHUNK_SIZE = 10;
+    let originalPayloadLimit: number;
+
+    beforeEach(() => {
+      const handlerConstructor = DcsHandler as unknown as { PAYLOAD_LIMIT: number };
+      originalPayloadLimit = handlerConstructor.PAYLOAD_LIMIT;
+      handlerConstructor.PAYLOAD_LIMIT = TEST_PAYLOAD_LIMIT;
+    });
+
+    afterEach(() => {
+      const handlerConstructor = DcsHandler as unknown as { PAYLOAD_LIMIT: number };
+      handlerConstructor.PAYLOAD_LIMIT = originalPayloadLimit;
+    });
+
     it('should be called once on end(true)', () => {
       parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
       parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
@@ -230,19 +244,19 @@ describe('DcsParser', () => {
       this.timeout(30000);
       parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
       parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
-      const data = toUtf32('A'.repeat(1000));
-      for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+      const data = toUtf32('A'.repeat(CHUNK_SIZE));
+      for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
         parser.put(data, 0, data.length);
       }
       parser.unhook(true);
-      assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(PAYLOAD_LIMIT)]]);
+      assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
     });
     it('should abort for payload limit +1', function(): void {
       this.timeout(30000);
       parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
       parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
-      let data = toUtf32('A'.repeat(1000));
-      for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+      let data = toUtf32('A'.repeat(CHUNK_SIZE));
+      for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
         parser.put(data, 0, data.length);
       }
       data = toUtf32('A');
diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts
index b88171c8..cad73352 100644
--- a/src/common/parser/OscParser.test.ts
+++ b/src/common/parser/OscParser.test.ts
@@ -6,7 +6,6 @@ import { assert } from 'chai';
 import { OscParser, OscHandler } from 'common/parser/OscParser';
 import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
 import { IOscHandler } from 'common/parser/Types';
-import { PAYLOAD_LIMIT } from 'common/parser/Constants';
 
 function toUtf32(s: string): Uint32Array {
   const utf32 = new Uint32Array(s.length);
@@ -170,6 +169,21 @@ describe('OscParser', () => {
     });
   });
   describe('OscHandlerFactory', () => {
+    const TEST_PAYLOAD_LIMIT = 100;
+    const CHUNK_SIZE = 10;
+    let originalPayloadLimit: number;
+
+    beforeEach(() => {
+      const handlerConstructor = OscHandler as unknown as { PAYLOAD_LIMIT: number };
+      originalPayloadLimit = handlerConstructor.PAYLOAD_LIMIT;
+      handlerConstructor.PAYLOAD_LIMIT = TEST_PAYLOAD_LIMIT;
+    });
+
+    afterEach(() => {
+      const handlerConstructor = OscHandler as unknown as { PAYLOAD_LIMIT: number };
+      handlerConstructor.PAYLOAD_LIMIT = originalPayloadLimit;
+    });
+
     it('should be called once on end(true)', () => {
       parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
       parser.start();
@@ -226,12 +240,12 @@ describe('OscParser', () => {
       parser.start();
       let data = toUtf32('1234;');
       parser.put(data, 0, data.length);
-      data = toUtf32('A'.repeat(1000));
-      for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+      data = toUtf32('A'.repeat(CHUNK_SIZE));
+      for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
         parser.put(data, 0, data.length);
       }
       parser.end(true);
-      assert.deepEqual(reports, [[1234, 'A'.repeat(PAYLOAD_LIMIT)]]);
+      assert.deepEqual(reports, [[1234, 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
     });
     it('should abort for payload limit +1', function(): void {
       this.timeout(30000);
@@ -239,8 +253,8 @@ describe('OscParser', () => {
       parser.start();
       let data = toUtf32('1234;');
       parser.put(data, 0, data.length);
-      data = toUtf32('A'.repeat(1000));
-      for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+      data = toUtf32('A'.repeat(CHUNK_SIZE));
+      for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
         parser.put(data, 0, data.length);
       }
       data = toUtf32('A');

From 0ca1bd48bec5d963d703777557a6ea2116c7c449 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:20:00 -0800
Subject: [PATCH 28/49] Inline payload limit

---
 src/common/parser/ApcParser.ts | 4 ++--
 src/common/parser/Constants.ts | 4 +++-
 src/common/parser/DcsParser.ts | 4 ++--
 src/common/parser/OscParser.ts | 4 ++--
 4 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/common/parser/ApcParser.ts b/src/common/parser/ApcParser.ts
index eff533e2..f7d3e6ed 100644
--- a/src/common/parser/ApcParser.ts
+++ b/src/common/parser/ApcParser.ts
@@ -4,7 +4,7 @@
  */
 
 import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from 'common/parser/Types';
-import { ApcState, PAYLOAD_LIMIT } from 'common/parser/Constants';
+import { ApcState, ParserConstants } from 'common/parser/Constants';
 import { utf32ToString } from 'common/input/TextDecoder';
 import { IDisposable } from 'common/Types';
 
@@ -199,7 +199,7 @@ export class ApcParser implements IApcParser {
  * as APC handlers.
  */
 export class ApcHandler implements IApcHandler {
-  private static PAYLOAD_LIMIT = PAYLOAD_LIMIT;
+  private static PAYLOAD_LIMIT = ParserConstants.PAYLOAD_LIMIT;
 
   private _data = '';
   private _hitLimit: boolean = false;
diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts
index 547e5407..828d7ad6 100644
--- a/src/common/parser/Constants.ts
+++ b/src/common/parser/Constants.ts
@@ -71,4 +71,6 @@ export const enum ApcState {
 }
 
 // payload limit for OSC and DCS
-export const PAYLOAD_LIMIT = 10000000;
+export const enum ParserConstants {
+  PAYLOAD_LIMIT = 10000000
+}
diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts
index 50253997..2478d94d 100644
--- a/src/common/parser/DcsParser.ts
+++ b/src/common/parser/DcsParser.ts
@@ -7,7 +7,7 @@ import { IDisposable } from 'common/Types';
 import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types';
 import { utf32ToString } from 'common/input/TextDecoder';
 import { Params } from 'common/parser/Params';
-import { PAYLOAD_LIMIT } from 'common/parser/Constants';
+import { ParserConstants } from 'common/parser/Constants';
 
 const EMPTY_HANDLERS: IDcsHandler[] = [];
 
@@ -138,7 +138,7 @@ EMPTY_PARAMS.addParam(0);
  * Note: The payload is currently limited to 50 MB (hardcoded).
  */
 export class DcsHandler implements IDcsHandler {
-  private static PAYLOAD_LIMIT = PAYLOAD_LIMIT;
+  private static PAYLOAD_LIMIT = ParserConstants.PAYLOAD_LIMIT;
 
   private _data = '';
   private _params: IParams = EMPTY_PARAMS;
diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts
index cea9e12c..ff85d874 100644
--- a/src/common/parser/OscParser.ts
+++ b/src/common/parser/OscParser.ts
@@ -4,7 +4,7 @@
  */
 
 import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types';
-import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
+import { OscState, ParserConstants } from 'common/parser/Constants';
 import { utf32ToString } from 'common/input/TextDecoder';
 import { IDisposable } from 'common/Types';
 
@@ -192,7 +192,7 @@ export class OscParser implements IOscParser {
  * as OSC handlers.
  */
 export class OscHandler implements IOscHandler {
-  private static PAYLOAD_LIMIT = PAYLOAD_LIMIT;
+  private static PAYLOAD_LIMIT = ParserConstants.PAYLOAD_LIMIT;
 
   private _data = '';
   private _hitLimit: boolean = false;

From 431d511c95bea32c7eb164e628cb0e3fee77d1a4 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:28:11 -0800
Subject: [PATCH 29/49] Fix benchmark usage, add instructions

---
 .../instructions/benchmark.instructions.md    | 16 +++++++++++++
 ...tructions.md => unit-test.instructions.md} |  0
 test/benchmark/Event.benchmark.ts             | 24 +++++++++----------
 3 files changed, 28 insertions(+), 12 deletions(-)
 create mode 100644 .github/instructions/benchmark.instructions.md
 rename .github/instructions/{unit-test-instructions.instructions.md => unit-test.instructions.md} (100%)

diff --git a/.github/instructions/benchmark.instructions.md b/.github/instructions/benchmark.instructions.md
new file mode 100644
index 00000000..94f5c7a7
--- /dev/null
+++ b/.github/instructions/benchmark.instructions.md
@@ -0,0 +1,16 @@
+---
+applyTo: '**/*.benchmark.ts'
+---
+# Benchmark run instructions
+
+- Full suite: `npm run benchmark`
+- Single benchmark file:
+  - Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js`
+  - Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js`
+- Single context/case:
+  - Use `-t` to get the path, then:
+  - `npm run benchmark -- -s "" out-test/benchmark/Event.benchmark.js`
+
+Notes:
+- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
+- Keep `NODE_PATH=./out` (handled by the npm script).
diff --git a/.github/instructions/unit-test-instructions.instructions.md b/.github/instructions/unit-test.instructions.md
similarity index 100%
rename from .github/instructions/unit-test-instructions.instructions.md
rename to .github/instructions/unit-test.instructions.md
diff --git a/test/benchmark/Event.benchmark.ts b/test/benchmark/Event.benchmark.ts
index 76f23f91..9cd8216d 100644
--- a/test/benchmark/Event.benchmark.ts
+++ b/test/benchmark/Event.benchmark.ts
@@ -3,7 +3,7 @@
  * @license MIT
  */
 
-import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
+import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
 import { Emitter } from 'common/Event';
 
 const ITERATIONS = 1_000_000;
@@ -14,12 +14,12 @@ perfContext('Emitter.fire()', () => {
     before(() => {
       emitter = new Emitter();
     });
-    new ThroughputRuntimeCase('', () => {
+    new RuntimeCase('', () => {
       for (let i = 0; i < ITERATIONS; i++) {
         emitter.fire(i);
       }
       return { payloadSize: ITERATIONS };
-    }, { fork: false }).showAverageThroughput();
+    }, { fork: false }).showAverageRuntime();
   });
 
   perfContext('1 listener', () => {
@@ -29,12 +29,12 @@ perfContext('Emitter.fire()', () => {
       emitter = new Emitter();
       emitter.event(e => { sum += e; });
     });
-    new ThroughputRuntimeCase('', () => {
+    new RuntimeCase('', () => {
       for (let i = 0; i < ITERATIONS; i++) {
         emitter.fire(i);
       }
-      return { payloadSize: ITERATIONS };
-    }, { fork: false }).showAverageThroughput();
+      return { payloadSize: ITERATIONS, sum };
+    }, { fork: false }).showAverageRuntime();
   });
 
   perfContext('2 listeners', () => {
@@ -45,12 +45,12 @@ perfContext('Emitter.fire()', () => {
       emitter.event(e => { sum += e; });
       emitter.event(e => { sum += e * 2; });
     });
-    new ThroughputRuntimeCase('', () => {
+    new RuntimeCase('', () => {
       for (let i = 0; i < ITERATIONS; i++) {
         emitter.fire(i);
       }
-      return { payloadSize: ITERATIONS };
-    }, { fork: false }).showAverageThroughput();
+      return { payloadSize: ITERATIONS, sum };
+    }, { fork: false }).showAverageRuntime();
   });
 
   perfContext('5 listeners', () => {
@@ -62,11 +62,11 @@ perfContext('Emitter.fire()', () => {
         emitter.event(e => { sum += e; });
       }
     });
-    new ThroughputRuntimeCase('', () => {
+    new RuntimeCase('', () => {
       for (let i = 0; i < ITERATIONS; i++) {
         emitter.fire(i);
       }
-      return { payloadSize: ITERATIONS };
-    }, { fork: false }).showAverageThroughput();
+      return { payloadSize: ITERATIONS, sum };
+    }, { fork: false }).showAverageRuntime();
   });
 });

From eed84230302b9ce01a138f517e3240038d9c6d0a Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:30:16 -0800
Subject: [PATCH 30/49] Clarify runtime cases

---
 .github/instructions/benchmark.instructions.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/instructions/benchmark.instructions.md b/.github/instructions/benchmark.instructions.md
index 94f5c7a7..2e586708 100644
--- a/.github/instructions/benchmark.instructions.md
+++ b/.github/instructions/benchmark.instructions.md
@@ -11,6 +11,8 @@ applyTo: '**/*.benchmark.ts'
   - Use `-t` to get the path, then:
   - `npm run benchmark -- -s "" out-test/benchmark/Event.benchmark.js`
 
+When writing instructions, use `RuntimeCase` to measure pure runtime in ms, use `ThroughputRuntimeCase` when measuring throughput in MB/s.
+
 Notes:
 - Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
 - Keep `NODE_PATH=./out` (handled by the npm script).

From ccbf612ddb51adf19c4b454eb13e482e9de8c005 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:33:49 -0800
Subject: [PATCH 31/49] Fix lint

---
 src/common/parser/ApcParser.test.ts | 10 +++++-----
 src/common/parser/ApcParser.ts      |  4 ++--
 src/common/parser/DcsParser.test.ts | 10 +++++-----
 src/common/parser/DcsParser.ts      |  4 ++--
 src/common/parser/OscParser.test.ts | 10 +++++-----
 src/common/parser/OscParser.ts      |  4 ++--
 6 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/src/common/parser/ApcParser.test.ts b/src/common/parser/ApcParser.test.ts
index f337e5c0..9021fb38 100644
--- a/src/common/parser/ApcParser.test.ts
+++ b/src/common/parser/ApcParser.test.ts
@@ -220,14 +220,14 @@ describe('ApcParser', () => {
     let originalPayloadLimit: number;
 
     beforeEach(() => {
-      const handlerConstructor = ApcHandler as unknown as { PAYLOAD_LIMIT: number };
-      originalPayloadLimit = handlerConstructor.PAYLOAD_LIMIT;
-      handlerConstructor.PAYLOAD_LIMIT = TEST_PAYLOAD_LIMIT;
+      const handlerConstructor = ApcHandler as unknown as { _payloadLimit: number };
+      originalPayloadLimit = handlerConstructor._payloadLimit;
+      handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
     });
 
     afterEach(() => {
-      const handlerConstructor = ApcHandler as unknown as { PAYLOAD_LIMIT: number };
-      handlerConstructor.PAYLOAD_LIMIT = originalPayloadLimit;
+      const handlerConstructor = ApcHandler as unknown as { _payloadLimit: number };
+      handlerConstructor._payloadLimit = originalPayloadLimit;
     });
 
     it('should be called once on end(true)', () => {
diff --git a/src/common/parser/ApcParser.ts b/src/common/parser/ApcParser.ts
index f7d3e6ed..dbcf0cce 100644
--- a/src/common/parser/ApcParser.ts
+++ b/src/common/parser/ApcParser.ts
@@ -199,7 +199,7 @@ export class ApcParser implements IApcParser {
  * as APC handlers.
  */
 export class ApcHandler implements IApcHandler {
-  private static PAYLOAD_LIMIT = ParserConstants.PAYLOAD_LIMIT;
+  private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
 
   private _data = '';
   private _hitLimit: boolean = false;
@@ -216,7 +216,7 @@ export class ApcHandler implements IApcHandler {
       return;
     }
     this._data += utf32ToString(data, start, end);
-    if (this._data.length > ApcHandler.PAYLOAD_LIMIT) {
+    if (this._data.length > ApcHandler._payloadLimit) {
       this._data = '';
       this._hitLimit = true;
     }
diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts
index 0c09199a..b63917f4 100644
--- a/src/common/parser/DcsParser.test.ts
+++ b/src/common/parser/DcsParser.test.ts
@@ -180,14 +180,14 @@ describe('DcsParser', () => {
     let originalPayloadLimit: number;
 
     beforeEach(() => {
-      const handlerConstructor = DcsHandler as unknown as { PAYLOAD_LIMIT: number };
-      originalPayloadLimit = handlerConstructor.PAYLOAD_LIMIT;
-      handlerConstructor.PAYLOAD_LIMIT = TEST_PAYLOAD_LIMIT;
+      const handlerConstructor = DcsHandler as unknown as { _payloadLimit: number };
+      originalPayloadLimit = handlerConstructor._payloadLimit;
+      handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
     });
 
     afterEach(() => {
-      const handlerConstructor = DcsHandler as unknown as { PAYLOAD_LIMIT: number };
-      handlerConstructor.PAYLOAD_LIMIT = originalPayloadLimit;
+      const handlerConstructor = DcsHandler as unknown as { _payloadLimit: number };
+      handlerConstructor._payloadLimit = originalPayloadLimit;
     });
 
     it('should be called once on end(true)', () => {
diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts
index 2478d94d..69220023 100644
--- a/src/common/parser/DcsParser.ts
+++ b/src/common/parser/DcsParser.ts
@@ -138,7 +138,7 @@ EMPTY_PARAMS.addParam(0);
  * Note: The payload is currently limited to 50 MB (hardcoded).
  */
 export class DcsHandler implements IDcsHandler {
-  private static PAYLOAD_LIMIT = ParserConstants.PAYLOAD_LIMIT;
+  private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
 
   private _data = '';
   private _params: IParams = EMPTY_PARAMS;
@@ -161,7 +161,7 @@ export class DcsHandler implements IDcsHandler {
       return;
     }
     this._data += utf32ToString(data, start, end);
-    if (this._data.length > DcsHandler.PAYLOAD_LIMIT) {
+    if (this._data.length > DcsHandler._payloadLimit) {
       this._data = '';
       this._hitLimit = true;
     }
diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts
index cad73352..8d5d4ace 100644
--- a/src/common/parser/OscParser.test.ts
+++ b/src/common/parser/OscParser.test.ts
@@ -174,14 +174,14 @@ describe('OscParser', () => {
     let originalPayloadLimit: number;
 
     beforeEach(() => {
-      const handlerConstructor = OscHandler as unknown as { PAYLOAD_LIMIT: number };
-      originalPayloadLimit = handlerConstructor.PAYLOAD_LIMIT;
-      handlerConstructor.PAYLOAD_LIMIT = TEST_PAYLOAD_LIMIT;
+      const handlerConstructor = OscHandler as unknown as { _payloadLimit: number };
+      originalPayloadLimit = handlerConstructor._payloadLimit;
+      handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
     });
 
     afterEach(() => {
-      const handlerConstructor = OscHandler as unknown as { PAYLOAD_LIMIT: number };
-      handlerConstructor.PAYLOAD_LIMIT = originalPayloadLimit;
+      const handlerConstructor = OscHandler as unknown as { _payloadLimit: number };
+      handlerConstructor._payloadLimit = originalPayloadLimit;
     });
 
     it('should be called once on end(true)', () => {
diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts
index ff85d874..1f0c8cb2 100644
--- a/src/common/parser/OscParser.ts
+++ b/src/common/parser/OscParser.ts
@@ -192,7 +192,7 @@ export class OscParser implements IOscParser {
  * as OSC handlers.
  */
 export class OscHandler implements IOscHandler {
-  private static PAYLOAD_LIMIT = ParserConstants.PAYLOAD_LIMIT;
+  private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
 
   private _data = '';
   private _hitLimit: boolean = false;
@@ -209,7 +209,7 @@ export class OscHandler implements IOscHandler {
       return;
     }
     this._data += utf32ToString(data, start, end);
-    if (this._data.length > OscHandler.PAYLOAD_LIMIT) {
+    if (this._data.length > OscHandler._payloadLimit) {
       this._data = '';
       this._hitLimit = true;
     }

From 34e68b1ad4218fd996d2b3197f1988007867bba2 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 15:50:46 -0800
Subject: [PATCH 32/49] Speed up all remaining unit tests below threshold
 (40ms?)

---
 src/browser/Terminal.test.ts         | 87 ++++++++++++++++------------
 src/common/InputHandler.test.ts      |  6 +-
 src/common/input/WriteBuffer.test.ts |  2 +-
 src/common/parser/DcsParser.test.ts  |  2 +-
 src/common/parser/OscParser.test.ts  |  2 +-
 5 files changed, 56 insertions(+), 43 deletions(-)

diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts
index be5ac614..66637e37 100644
--- a/src/browser/Terminal.test.ts
+++ b/src/browser/Terminal.test.ts
@@ -190,9 +190,7 @@ describe('Terminal', () => {
     });
     it('should clear a buffer larger than rows', async () => {
       // Fill the buffer with dummy rows
-      for (let i = 0; i < term.rows * 2; i++) {
-        await term.writeP('test\n');
-      }
+      await term.writeP('test\n'.repeat(term.rows * 2));
 
       const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
       term.clear();
@@ -389,9 +387,7 @@ describe('Terminal', () => {
 
       it('should not scroll down, when a custom keydown handler prevents the event', async () => {
         // Add some output to the terminal
-        for (let i = 0; i < term.rows * 3; i++) {
-          await term.writeP('test\r\n');
-        }
+        await term.writeP('test\r\n'.repeat(term.rows * 3));
         const startYDisp = (term.rows * 2) + 1;
         term.attachCustomKeyEventHandler(() => {
           return false;
@@ -731,70 +727,87 @@ describe('Terminal', () => {
       it(`${range}: 2 characters per cell`, async function (): Promise {
         const high = String.fromCharCode(0xD800);
         const cell = new CellData();
+        const values: string[] = [];
         for (let j = i; j <= i + 0xF; j++) {
-          await term.writeP(high + String.fromCharCode(j));
-          const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
-          assert.equal(tchar.getChars(), high + String.fromCharCode(j));
+          values.push(high + String.fromCharCode(j));
+        }
+        await term.writeP(values.join('\r\n'));
+        for (let idx = 0; idx < values.length; idx++) {
+          const expected = values[idx];
+          const tchar = term.buffer.lines.get(idx)!.loadCell(0, cell);
+          assert.equal(tchar.getChars(), expected);
           assert.equal(tchar.getChars().length, 2);
           assert.equal(tchar.getWidth(), 1);
-          assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
-          term.reset();
+          assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
         }
       });
       it(`${range}: 2 characters at last cell`, async () => {
         const high = String.fromCharCode(0xD800);
         const cell = new CellData();
-        term.buffer.x = term.cols - 1;
+        const values: string[] = [];
         for (let j = i; j <= i + 0xF; j++) {
-          await term.writeP(high + String.fromCharCode(j));
-          assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(j));
-          assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
-          assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
-          term.reset();
+          values.push(high + String.fromCharCode(j));
+        }
+        await term.writeP(values.map((value, idx) => `\x1b[${idx + 1};${term.cols}H${value}`).join(''));
+        for (let idx = 0; idx < values.length; idx++) {
+          const expected = values[idx];
+          assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars(), expected);
+          assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars().length, 2);
+          assert.equal(term.buffer.lines.get(idx + 1)!.loadCell(0, cell).getChars(), '');
         }
       });
       it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise {
         const high = String.fromCharCode(0xD800);
         const cell = new CellData();
+        term.resize(term.cols, 40);
+        const values: string[] = [];
         for (let j = i; j <= i + 0xF; j++) {
-          term.buffer.x = term.cols - 1;
-          await term.writeP('a' + high + String.fromCharCode(j));
-          assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
-          assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(j));
-          assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
-          assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
-          term.reset();
+          values.push(high + String.fromCharCode(j));
+        }
+        await term.writeP(values.map((value, idx) => `\x1b[${idx * 2 + 1};${term.cols}H` + 'a' + value).join(''));
+        for (let idx = 0; idx < values.length; idx++) {
+          const expected = values[idx];
+          const row = idx * 2;
+          assert.equal(term.buffer.lines.get(row)!.loadCell(term.cols - 1, cell).getChars(), 'a');
+          assert.equal(term.buffer.lines.get(row + 1)!.loadCell(0, cell).getChars(), expected);
+          assert.equal(term.buffer.lines.get(row + 1)!.loadCell(0, cell).getChars().length, 2);
+          assert.equal(term.buffer.lines.get(row + 1)!.loadCell(1, cell).getChars(), '');
         }
       });
       it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise {
         const high = String.fromCharCode(0xD800);
         const cell = new CellData();
+        const values: string[] = [];
         for (let j = i; j <= i + 0xF; j++) {
-          term.buffer.x = term.cols - 1;
-          await term.writeP('\x1b[?7l'); // Disable wraparound mode
           const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000);
           if (width !== 1) {
             continue;
           }
-          await term.writeP('a' + high + String.fromCharCode(j));
-          // auto wraparound mode should cut off the rest of the line
-          assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(j));
-          assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
-          assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
-          term.reset();
+          values.push(high + String.fromCharCode(j));
+        }
+        await term.writeP('\x1b[?7l' + values.map((value, idx) => `\x1b[${idx + 1};${term.cols}H` + 'a' + value).join(''));
+        for (let idx = 0; idx < values.length; idx++) {
+          const expected = values[idx];
+          assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars(), expected);
+          assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars().length, 2);
+          assert.equal(term.buffer.lines.get(idx + 1)!.loadCell(1, cell).getChars(), '');
         }
       });
       it(`${range}: splitted surrogates`, async function (): Promise {
         const high = String.fromCharCode(0xD800);
         const cell = new CellData();
+        const values: string[] = [];
         for (let j = i; j <= i + 0xF; j++) {
-          await term.writeP(high + String.fromCharCode(j));
-          const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
-          assert.equal(tchar.getChars(), high + String.fromCharCode(j));
+          values.push(high + String.fromCharCode(j));
+        }
+        await term.writeP(values.join('\r\n'));
+        for (let idx = 0; idx < values.length; idx++) {
+          const expected = values[idx];
+          const tchar = term.buffer.lines.get(idx)!.loadCell(0, cell);
+          assert.equal(tchar.getChars(), expected);
           assert.equal(tchar.getChars().length, 2);
           assert.equal(tchar.getWidth(), 1);
-          assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
-          term.reset();
+          assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
         }
       });
     }
diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts
index fc02715e..d7026e50 100644
--- a/src/common/InputHandler.test.ts
+++ b/src/common/InputHandler.test.ts
@@ -2521,7 +2521,7 @@ describe('InputHandler', () => {
       const cpr: number[][] = [];
       inputHandler.registerCsiHandler({ final: 'H' }, async params => {
         cup.push(params.toArray() as number[]);
-        await new Promise(res => setTimeout(res, 50));
+        await Promise.resolve();
         // late call of real repositioning
         return inputHandler.cursorPosition(params);
       });
@@ -2536,7 +2536,7 @@ describe('InputHandler', () => {
     });
     it('async OSC between', async () => {
       inputHandler.registerOscHandler(1000, async data => {
-        await new Promise(res => setTimeout(res, 50));
+        await Promise.resolve();
         assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
         assert.equal(data, 'some data');
         return true;
@@ -2546,7 +2546,7 @@ describe('InputHandler', () => {
     });
     it('async DCS between', async () => {
       inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
-        await new Promise(res => setTimeout(res, 50));
+        await Promise.resolve();
         assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
         assert.equal(data, 'some data');
         assert.deepEqual(params.toArray(), [1, 2]);
diff --git a/src/common/input/WriteBuffer.test.ts b/src/common/input/WriteBuffer.test.ts
index c534bbae..f366a632 100644
--- a/src/common/input/WriteBuffer.test.ts
+++ b/src/common/input/WriteBuffer.test.ts
@@ -88,7 +88,7 @@ describe('WriteBuffer', () => {
     it('writeSync called from action does not overflow callstack - issue #3265', () => {
       wb = new WriteBuffer(data => {
         const num = parseInt(data as string);
-        if (num < 1000000) {
+        if (num < 10000) {
           wb.writeSync('' + (num + 1));
         }
       });
diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts
index b63917f4..0296fa88 100644
--- a/src/common/parser/DcsParser.test.ts
+++ b/src/common/parser/DcsParser.test.ts
@@ -278,7 +278,7 @@ class TestHandlerAsync implements IDcsHandler {
   }
   public async unhook(success: boolean): Promise {
     // simple sleep to check in tests whether ordering gets messed up
-    await new Promise(res => setTimeout(res, 20));
+    await Promise.resolve();
     this.output.push([this.msg, 'UNHOOK', success]);
     if (this.returnFalse) {
       return false;
diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts
index 8d5d4ace..b6b96cba 100644
--- a/src/common/parser/OscParser.test.ts
+++ b/src/common/parser/OscParser.test.ts
@@ -275,7 +275,7 @@ class TestHandlerAsync implements IOscHandler {
     this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
   }
   public async end(success: boolean): Promise {
-    await new Promise(res => setTimeout(res, 20));
+    await Promise.resolve();
     this.output.push([this.msg, this.id, 'END', success]);
     if (this.returnFalse) {
       return false;

From 7a2794a5525904f1ebfcd37cdf7364d925dff110 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 16:21:03 -0800
Subject: [PATCH 33/49] Clear dangling timeout that keeps unit tests event loop
 alive

This fixes the issue where test-unit could run for up to 5 seconds after
the tests have already finished.
---
 src/common/InputHandler.ts | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index 11381ce0..ca07a425 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -407,8 +407,19 @@ export class InputHandler extends Disposable implements IInputHandler {
   private _logSlowResolvingAsync(p: Promise): void {
     // log a limited warning about an async handler taking too long
     if (this._logService.logLevel <= LogLevelEnum.WARN) {
-      Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))])
-        .catch(err => {
+      let slowTimeout: ReturnType | undefined;
+      const slowPromise = new Promise((_res, rej) => {
+        slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT);
+      });
+      Promise.race([p, slowPromise])
+        .then(() => {
+          if (slowTimeout !== undefined) {
+            clearTimeout(slowTimeout);
+          }
+        }, err => {
+          if (slowTimeout !== undefined) {
+            clearTimeout(slowTimeout);
+          }
           if (err !== '#SLOW_TIMEOUT') {
             throw err;
           }

From 2c0ba3c05815f9771bebe9607015ddca97ba65c6 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 16:40:55 -0800
Subject: [PATCH 34/49] Speed up TextDecoder.test.ts

- Reduce calls to decode and clear
- Consolidate numberic checks
- Increase batch size
---
 src/common/input/TextDecoder.test.ts | 118 +++++++++++++++++----------
 1 file changed, 75 insertions(+), 43 deletions(-)

diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts
index abf9e47f..a0a4a323 100644
--- a/src/common/input/TextDecoder.test.ts
+++ b/src/common/input/TextDecoder.test.ts
@@ -29,7 +29,46 @@ function fromByteString(s: string): Uint8Array {
   return result;
 }
 
-const BATCH_SIZE = 2048;
+function assertDecodedRange(
+  min: number,
+  max: number,
+  skip: (codePoint: number) => boolean,
+  buildChar: (codePoint: number) => string,
+  decode: (input: string, target: Uint32Array) => number,
+  outputToString: (data: Uint32Array, length: number) => string
+): void {
+  if (max <= min) {
+    return;
+  }
+  let input = '';
+  let count = 0;
+  for (let i = min; i < max; ++i) {
+    if (skip(i)) {
+      continue;
+    }
+    input += buildChar(i);
+    count++;
+  }
+  const target = new Uint32Array(count);
+  const length = decode(input, target);
+  assert.equal(length, count);
+  let mismatchIndex = -1;
+  let index = 0;
+  for (let i = min; i < max; ++i) {
+    if (skip(i)) {
+      continue;
+    }
+    if (target[index] !== i) {
+      mismatchIndex = index;
+      break;
+    }
+    index++;
+  }
+  assert.equal(mismatchIndex, -1);
+  assert.equal(outputToString(target, length), input);
+}
+
+const BATCH_SIZE = 8192;
 
 const TEST_STRINGS = [
   'Лорем ипсум долор сит амет, ех сеа аццусам диссентиет. Ан еос стет еирмод витуперата. Иус дицерет урбанитас ет. Ан при алтера долорес сплендиде, цу яуо интегре денияуе, игнота волуптариа инструцтиор цу вим.',
@@ -60,34 +99,31 @@ describe('text encodings', () => {
         const max = Math.min(min + BATCH_SIZE, 65536);
         it(`${formatRange(min, max)}`, () => {
           const decoder = new StringToUtf32();
-          const target = new Uint32Array(5);
-          for (let i = min; i < max; ++i) {
-            // skip surrogate pairs and a BOM
-            if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
-              continue;
-            }
-            const length = decoder.decode(String.fromCharCode(i), target);
-            assert.equal(length, 1);
-            assert.equal(target[0], i);
-            assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i));
-            decoder.clear();
-          }
+          assertDecodedRange(
+            min,
+            max,
+            (i) => (i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF,
+            (i) => String.fromCharCode(i),
+            (input, target) => decoder.decode(input, target),
+            (data, length) => utf32ToString(data, 0, length)
+          );
         });
       }
       for (let min = 65536; min < 0x10FFFF; min += BATCH_SIZE) {
         const max = Math.min(min + BATCH_SIZE, 0x10FFFF);
         it(`${formatRange(min, max)} (surrogates)`, () => {
           const decoder = new StringToUtf32();
-          const target = new Uint32Array(5);
-          for (let i = min; i < max; ++i) {
-            const codePoint = i - 0x10000;
-            const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
-            const length = decoder.decode(s, target);
-            assert.equal(length, 1);
-            assert.equal(target[0], i);
-            assert.equal(utf32ToString(target, 0, length), s);
-            decoder.clear();
-          }
+          assertDecodedRange(
+            min,
+            max,
+            () => false,
+            (i) => {
+              const codePoint = i - 0x10000;
+              return String.fromCharCode((codePoint >> 10) + 0xD800, (codePoint % 0x400) + 0xDC00);
+            },
+            (input, target) => decoder.decode(input, target),
+            (data, length) => utf32ToString(data, 0, length)
+          );
         });
       }
 
@@ -131,18 +167,14 @@ describe('text encodings', () => {
         const max = Math.min(min + BATCH_SIZE, 65536);
         it(`${formatRange(min, max)} (1/2/3 byte sequences)`, () => {
           const decoder = new Utf8ToUtf32();
-          const target = new Uint32Array(5);
-          for (let i = min; i < max; ++i) {
-            // skip surrogate pairs and a BOM
-            if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
-              continue;
-            }
-            const utf8Data = fromByteString(encode(String.fromCharCode(i)));
-            const length = decoder.decode(utf8Data, target);
-            assert.equal(length, 1);
-            assert.equal(toString(target, length), String.fromCharCode(i));
-            decoder.clear();
-          }
+          assertDecodedRange(
+            min,
+            max,
+            (i) => (i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF,
+            (i) => String.fromCharCode(i),
+            (input, target) => decoder.decode(fromByteString(encode(input)), target),
+            (data, length) => toString(data, length)
+          );
         });
       }
       for (let minRaw = 60000; minRaw < 0x10FFFF; minRaw += BATCH_SIZE) {
@@ -150,14 +182,14 @@ describe('text encodings', () => {
         const max = Math.min(minRaw + BATCH_SIZE, 0x10FFFF);
         it(`${formatRange(min, max)} (4 byte sequences)`, function (): void {
           const decoder = new Utf8ToUtf32();
-          const target = new Uint32Array(5);
-          for (let i = min; i < max; ++i) {
-            const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
-            const length = decoder.decode(utf8Data, target);
-            assert.equal(length, 1);
-            assert.equal(target[0], i);
-            decoder.clear();
-          }
+          assertDecodedRange(
+            min,
+            max,
+            () => false,
+            (i) => stringFromCodePoint(i),
+            (input, target) => decoder.decode(fromByteString(encode(input)), target),
+            (data, length) => toString(data, length)
+          );
         });
       }
 

From 51b9a713da082d518920951bb969bb460c4d151a Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 18:21:32 -0800
Subject: [PATCH 35/49] Implement more InputHandler tests

Part of #2117
---
 test/playwright/InputHandler.test.ts | 433 +++++++++++++++++++++------
 1 file changed, 337 insertions(+), 96 deletions(-)

diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts
index a7d1bc77..55b63dc9 100644
--- a/test/playwright/InputHandler.test.ts
+++ b/test/playwright/InputHandler.test.ts
@@ -274,8 +274,9 @@ test.describe('InputHandler Integration Tests', () => {
       await ctx.proxy.write('\x1b[10`b');
       await pollFor(ctx.page, () => getLinesAsArray(1), ['aoo      b']);
     });
-    test.skip('CSI Ps a - ', async () => {
-      // TODO: Implement
+    test('CSI Ps a - HPR: Character Position Relative (default = [row,col+1])', async () => {
+      await ctx.proxy.write('a\x1b[2aB');
+      await pollFor(ctx.page, () => getLinesAsArray(1), ['a  B']);
     });
     test('CSI Ps b - REP: Repeat preceding character, ECMA48', async () => {
       // default to 1
@@ -356,18 +357,32 @@ test.describe('InputHandler Integration Tests', () => {
       // With all tabs cleared, tab moves to end of line
       await pollFor(ctx.page, () => getLinesAsArray(1), ['                                                                               a']);
     });
-    test.skip('CSI Ps h - ', async () => {
-      // TODO: Implement
+    test('CSI Ps h - SM: Set Mode', async () => {
+      await ctx.proxy.write('\x1b[4h');
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).insertMode, true);
+      await ctx.proxy.write('\x1b[20h');
+      await pollFor(ctx.page, async () => await ctx.proxy.getOption('convertEol'), true);
     });
     test.describe('CSI ? Pm h - DECSET: Private Mode Set', () => {
-      test.skip('Ps = 1 - Application Cursor Keys (DECCKM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 1 - Application Cursor Keys (DECCKM), VT100', async () => {
+        await ctx.proxy.write('\x1b[?1h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationCursorKeysMode, true);
+        recordedData.length = 0;
+        await ctx.proxy.focus();
+        await ctx.page.keyboard.press('ArrowUp');
+        await pollFor(ctx.page, () => recordedData, ['\x1bOA']);
       });
-      test.skip('Ps = 2 - Designate USASCII for character sets G0-G3 (DECANM), VT100, and set VT100 mode', async () => {
-      // TODO: Implement
+      test('Ps = 2 - Designate USASCII for character sets G0-G3 (DECANM), VT100, and set VT100 mode', async () => {
+        await ctx.proxy.write('\x1b(0q\x1b[?2hq');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['─q']);
       });
-      test.skip('Ps = 3 - 132 Column Mode (DECCOLM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 3 - 132 Column Mode (DECCOLM), VT100', async () => {
+        const windowOptions = await ctx.proxy.getOption('windowOptions');
+        await ctx.proxy.setOption('windowOptions', { ...windowOptions, setWinLines: true });
+        await ctx.proxy.write('\x1b[?3h');
+        await pollFor(ctx.page, async () => await ctx.proxy.cols, 132);
+        await ctx.proxy.resize(80, 24);
+        await ctx.proxy.setOption('windowOptions', windowOptions);
       });
       test.skip('Ps = 4 - Smooth (Slow) Scroll (DECSCLM), VT100', async () => {
       // TODO: Implement
@@ -375,17 +390,37 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 5 - Reverse Video (DECSCNM), VT100', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 6 - Origin Mode (DECOM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 6 - Origin Mode (DECOM), VT100', async () => {
+        await ctx.proxy.write('\x1b[?6h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).originMode, true);
+        await pollFor(ctx.page, () => getCursor(), { col: 0, row: 0 });
+        await ctx.proxy.write('\x1b[2;3r');
+        await ctx.proxy.write('\x1b[1;1HX');
+        await pollFor(ctx.page, () => getLinesAsArray(3), ['', 'X', '']);
       });
-      test.skip('Ps = 7 - Auto-Wrap Mode (DECAWM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 7 - Auto-Wrap Mode (DECAWM), VT100', async () => {
+        await ctx.proxy.resize(5, 2);
+        await ctx.proxy.write('\x1b[?7h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).wraparoundMode, true);
+        await ctx.proxy.write('12345X');
+        await pollFor(ctx.page, () => getLinesAsArray(2), ['12345', 'X']);
+        await ctx.proxy.reset();
+        await ctx.proxy.write('\x1b[?7l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).wraparoundMode, false);
+        await ctx.proxy.write('12345X');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['1234X']);
+        await ctx.proxy.resize(80, 24);
       });
       test.skip('Ps = 8 - Auto-Repeat Keys (DECARM), VT100', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 9 - Send Mouse X & Y on button press', async () => {
-      // TODO: Implement
+      test('Ps = 9 - Send Mouse X & Y on button press', async () => {
+        const selectionBefore = await dragSelection();
+        ok(selectionBefore > 0);
+        await ctx.proxy.write('\x1b[?9h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'x10');
+        const selectionAfter = await dragSelection();
+        ok(selectionAfter === 0);
       });
       test.skip('Ps = 1 0 - Show toolbar (rxvt)', async () => {
       // TODO: Implement
@@ -405,8 +440,11 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 9 - Set print extent to full screen (DECPEX), VT220', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 2 5 - Show cursor (DECTCEM), VT220', async () => {
-      // TODO: Implement
+      test('Ps = 2 5 - Show cursor (DECTCEM), VT220', async () => {
+        await ctx.proxy.write('\x1b[?25l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).showCursor, false);
+        await ctx.proxy.write('\x1b[?25h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).showCursor, true);
       });
       test.skip('Ps = 3 0 - Show scrollbar (rxvt)', async () => {
       // TODO: Implement
@@ -435,8 +473,15 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 4 4 - Enable Graphic Print Color Mode (DECGPCM), VT340', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 4 5 - Reverse-wraparound mode (XTREVWRAP), xterm', async () => {
-      // TODO: Implement
+      test('Ps = 4 5 - Reverse-wraparound mode (XTREVWRAP), xterm', async () => {
+        await ctx.proxy.write('\x1b[?45h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).reverseWraparoundMode, true);
+        await ctx.proxy.resize(5, 2);
+        await ctx.proxy.write('\x1b[?7h');
+        await ctx.proxy.write('12345X');
+        await ctx.proxy.write('\r\bY');
+        await pollFor(ctx.page, () => getLinesAsArray(2), ['1234Y', 'X']);
+        await ctx.proxy.resize(80, 24);
       });
       test.skip('Ps = 4 5 - Enable Graphic Print Color Syntax (DECGPCS), VT340', async () => {
       // TODO: Implement
@@ -447,14 +492,24 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 4 6 - Graphic Print Background Mode, VT340', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 4 7 - Use Alternate Screen Buffer, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 4 7 - Use Alternate Screen Buffer, xterm', async () => {
+        await ctx.proxy.write('main');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
+        await ctx.proxy.write('\x1b[?47h');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'alternate');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['']);
+        await ctx.proxy.write('\x1b[Halt');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['alt']);
+        await ctx.proxy.write('\x1b[?47l');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
       });
       test.skip('Ps = 4 7 - Enable Graphic Rotated Print Mode (DECGRPM), VT340', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 6 6 - Application keypad mode (DECNKM), VT320', async () => {
-      // TODO: Implement
+      test('Ps = 6 6 - Application keypad mode (DECNKM), VT320', async () => {
+        await ctx.proxy.write('\x1b[?66h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationKeypadMode, true);
       });
       test.skip('Ps = 6 7 - Backarrow key sends backspace (DECBKM), VT340, VT420', async () => {
       // TODO: Implement
@@ -468,14 +523,24 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 9 5 - Do not clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 1 0 0 0 - Send Mouse X & Y on button press and release', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 0 - Send Mouse X & Y on button press and release', async () => {
+        const selectionBefore = await dragSelection();
+        ok(selectionBefore > 0);
+        await ctx.proxy.write('\x1b[?1000h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'vt200');
+        const selectionAfter = await dragSelection();
+        ok(selectionAfter === 0);
       });
       test.skip('Ps = 1 0 0 1 - Use Hilite Mouse Tracking, xterm', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 1 0 0 2 - Use Cell Motion Mouse Tracking, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 2 - Use Cell Motion Mouse Tracking, xterm', async () => {
+        const selectionBefore = await dragSelection();
+        ok(selectionBefore > 0);
+        await ctx.proxy.write('\x1b[?1002h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'drag');
+        const selectionAfter = await dragSelection();
+        ok(selectionAfter === 0);
       });
       test('Ps = 1 0 0 3 - Set Use All Motion (any event) Mouse Tracking', async () => {
         const coords: { left: number, top: number, bottom: number, right: number } = await ctx.page.evaluate(`
@@ -503,14 +568,21 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, async () => (await ctx.proxy.getSelection()).length, 0);
         await ctx.page.mouse.up();
       });
-      test.skip('Ps = 1 0 0 4 - Send FocusIn/FocusOut events, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 4 - Send FocusIn/FocusOut events, xterm', async () => {
+        await ctx.proxy.write('\x1b[?1004h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).sendFocusMode, true);
+        await ctx.proxy.blur();
+        recordedData.length = 0;
+        await ctx.proxy.focus();
+        await ctx.proxy.blur();
+        await pollFor(ctx.page, () => recordedData, ['\x1b[I', '\x1b[O']);
       });
       test.skip('Ps = 1 0 0 5 - Enable UTF-8 Mouse Mode, xterm', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 1 0 0 6 - Enable SGR Mouse Mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 6 - Enable SGR Mouse Mode, xterm', async () => {
+        await ctx.proxy.write('\x1b[?1006h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
       });
       test.skip('Ps = 1 0 0 7 - Enable Alternate Scroll Mode, xterm', async () => {
       // TODO: Implement
@@ -524,8 +596,9 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 0 1 5 - Enable urxvt Mouse Mode', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 1 0 1 6 - Enable SGR Mouse PixelMode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 1 6 - Enable SGR Mouse PixelMode, xterm', async () => {
+        await ctx.proxy.write('\x1b[?1016h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR_PIXELS');
       });
       test.skip('Ps = 1 0 3 4 - Interpret "meta" key, xterm', async () => {
       // TODO: Implement
@@ -563,11 +636,24 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 0 4 6 - Enable switching to/from Alternate Screen Buffer, xterm', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 1 0 4 7 - Use Alternate Screen Buffer, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 7 - Use Alternate Screen Buffer, xterm', async () => {
+        await ctx.proxy.write('main');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
+        await ctx.proxy.write('\x1b[?1047h');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'alternate');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['']);
+        await ctx.proxy.write('\x1b[Halt');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['alt']);
+        await ctx.proxy.write('\x1b[?1047l');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
       });
-      test.skip('Ps = 1 0 4 8 - Save cursor as in DECSC, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 8 - Save cursor as in DECSC, xterm', async () => {
+        await ctx.proxy.write('\x1b[4;5H');
+        await ctx.proxy.write('\x1b[?1048h');
+        await ctx.proxy.write('\x1b[1;1H');
+        await ctx.proxy.write('\x1b[?1048l');
+        await pollFor(ctx.page, () => getCursor(), { col: 4, row: 3 });
       });
       test.skip('Ps = 1 0 4 9 - Save cursor as in DECSC, xterm', async () => {
       // TODO: Implement
@@ -623,12 +709,20 @@ test.describe('InputHandler Integration Tests', () => {
     test.skip('CSI ? Ps i - MC: Media Copy, DEC-specified', async () => {
       // TODO: Implement
     });
-    test.skip('CSI Pm l - RM: Reset Mode', async () => {
-      // TODO: Implement
+    test('CSI Pm l - RM: Reset Mode', async () => {
+      await ctx.proxy.write('\x1b[4h\x1b[20h');
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).insertMode, true);
+      await pollFor(ctx.page, async () => await ctx.proxy.getOption('convertEol'), true);
+      await ctx.proxy.write('\x1b[4l\x1b[20l');
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).insertMode, false);
+      await pollFor(ctx.page, async () => await ctx.proxy.getOption('convertEol'), false);
     });
     test.describe('CSI ? Pm l - DECRST: DEC Private Mode Reset', async () => {
-      test.skip('Ps = 1 - Normal Cursor Keys (DECCKM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 1 - Normal Cursor Keys (DECCKM), VT100.', async () => {
+        await ctx.proxy.write('\x1b[?1h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationCursorKeysMode, true);
+        await ctx.proxy.write('\x1b[?1l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationCursorKeysMode, false);
       });
       test.skip('Ps = 2 - Designate VT52 mode (DECANM), VT100.', async () => {
         // TODO: Implement
@@ -642,17 +736,36 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 5 - Normal Video (DECSCNM), VT100.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 6 - Normal Cursor Mode (DECOM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 6 - Normal Cursor Mode (DECOM), VT100.', async () => {
+        await ctx.proxy.write('\x1b[?6h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).originMode, true);
+        await ctx.proxy.write('\x1b[?6l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).originMode, false);
       });
-      test.skip('Ps = 7 - No Auto-Wrap Mode (DECAWM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 7 - No Auto-Wrap Mode (DECAWM), VT100.', async () => {
+        await ctx.proxy.resize(5, 2);
+        await ctx.proxy.write('\x1b[?7h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).wraparoundMode, true);
+        await ctx.proxy.write('\x1b[?7l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).wraparoundMode, false);
+        await ctx.proxy.write('12345X');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['1234X']);
+        await ctx.proxy.write('\x1b[?7h');
+        await ctx.proxy.reset();
+        await ctx.proxy.write('\x1b[?7h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).wraparoundMode, true);
+        await ctx.proxy.write('12345X');
+        await pollFor(ctx.page, () => getLinesAsArray(2), ['12345', 'X']);
+        await ctx.proxy.resize(80, 24);
       });
       test.skip('Ps = 8 - No Auto-Repeat Keys (DECARM), VT100.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 9 - Don\'t send Mouse X & Y on button press, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 9 - Don\'t send Mouse X & Y on button press, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?9h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'x10');
+        await ctx.proxy.write('\x1b[?9l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'none');
       });
       test.skip('Ps = 1 0 - Hide toolbar (rxvt).', async () => {
         // TODO: Implement
@@ -672,8 +785,11 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 9 - Limit print to scrolling region (DECPEX), VT220.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 2 5 - Hide cursor (DECTCEM), VT220.', async () => {
-        // TODO: Implement
+      test('Ps = 2 5 - Hide cursor (DECTCEM), VT220.', async () => {
+        await ctx.proxy.write('\x1b[?25h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).showCursor, true);
+        await ctx.proxy.write('\x1b[?25l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).showCursor, false);
       });
       test.skip('Ps = 3 0 - Don\'t show scrollbar (rxvt).', async () => {
         // TODO: Implement
@@ -699,8 +815,11 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 4 4 - Disable Graphic Print Color Mode (DECGPCM), VT340.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 4 5 - No Reverse-wraparound mode (XTREVWRAP), xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 4 5 - No Reverse-wraparound mode (XTREVWRAP), xterm.', async () => {
+        await ctx.proxy.write('\x1b[?45h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).reverseWraparoundMode, true);
+        await ctx.proxy.write('\x1b[?45l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).reverseWraparoundMode, false);
       });
       test.skip('Ps = 4 5 - Disable Graphic Print Color Syntax (DECGPCS), VT340.', async () => {
         // TODO: Implement
@@ -708,14 +827,20 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 4 6 - Stop logging (XTLOGGING), xterm.  This is normally disabled by a compile-time option.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 4 7 - Use Normal Screen Buffer, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 4 7 - Use Normal Screen Buffer, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?47h');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'alternate');
+        await ctx.proxy.write('\x1b[?47l');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
       });
       test.skip('Ps = 4 7 - Disable Graphic Rotated Print Mode (DECGRPM), VT340.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 6 6 - Numeric keypad mode (DECNKM), VT320.', async () => {
-        // TODO: Implement
+      test('Ps = 6 6 - Numeric keypad mode (DECNKM), VT320.', async () => {
+        await ctx.proxy.write('\x1b[?66h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationKeypadMode, true);
+        await ctx.proxy.write('\x1b[?66l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationKeypadMode, false);
       });
       test.skip('Ps = 6 7 - Backarrow key sends delete (DECBKM), VT340, VT420.  This sets the backarrowKey resource to "false".', async () => {
         // TODO: Implement
@@ -729,26 +854,41 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 9 5 - Clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 1 0 0 0 - Don\'t send Mouse X & Y on button press and release.  See the section Mouse Tracking.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 0 - Don\'t send Mouse X & Y on button press and release.  See the section Mouse Tracking.', async () => {
+        await ctx.proxy.write('\x1b[?1000h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'vt200');
+        await ctx.proxy.write('\x1b[?1000l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'none');
       });
       test.skip('Ps = 1 0 0 1 - Don\'t use Hilite Mouse Tracking, xterm.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 1 0 0 2 - Don\'t use Cell Motion Mouse Tracking, xterm.  See the section Button-event tracking.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 2 - Don\'t use Cell Motion Mouse Tracking, xterm.  See the section Button-event tracking.', async () => {
+        await ctx.proxy.write('\x1b[?1002h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'drag');
+        await ctx.proxy.write('\x1b[?1002l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'none');
       });
-      test.skip('Ps = 1 0 0 3 - Don\'t use All Motion Mouse Tracking, xterm. See the section Any-event tracking.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 3 - Don\'t use All Motion Mouse Tracking, xterm. See the section Any-event tracking.', async () => {
+        await ctx.proxy.write('\x1b[?1003h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'any');
+        await ctx.proxy.write('\x1b[?1003l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'none');
       });
-      test.skip('Ps = 1 0 0 4 - Don\'t send FocusIn/FocusOut events, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 4 - Don\'t send FocusIn/FocusOut events, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?1004h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).sendFocusMode, true);
+        await ctx.proxy.write('\x1b[?1004l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).sendFocusMode, false);
       });
       test.skip('Ps = 1 0 0 5 - Disable UTF-8 Mouse Mode, xterm.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 1 0 0 6 - Disable SGR Mouse Mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 6 - Disable SGR Mouse Mode, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?1006h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
+        await ctx.proxy.write('\x1b[?1006l');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'DEFAULT');
       });
       test.skip('Ps = 1 0 0 7 - Disable Alternate Scroll Mode, xterm.  This corresponds to the alternateScroll resource.', async () => {
         // TODO: Implement
@@ -762,8 +902,11 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 0 1 5 - Disable urxvt Mouse Mode.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 1 0 1 6 - Disable SGR Mouse Pixel-Mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 1 6 - Disable SGR Mouse Pixel-Mode, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?1016h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR_PIXELS');
+        await ctx.proxy.write('\x1b[?1016l');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'DEFAULT');
       });
       test.skip('Ps = 1 0 3 4 - Don\'t interpret "meta" key, xterm.  This disables the eightBitInput resource.', async () => {
         // TODO: Implement
@@ -798,14 +941,28 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 0 4 6 - Disable switching to/from Alternate Screen Buffer, xterm.  This works for terminfo-based systems, updating the titeInhibit resource.  If currently using the Alternate Screen Buffer, xterm switches to the Normal Screen Buffer.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 1 0 4 7 - Use Normal Screen Buffer, xterm.  Clear the screen first if in the Alternate Screen Buffer.  This may be disabled by the titeInhibit resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 7 - Use Normal Screen Buffer, xterm.  Clear the screen first if in the Alternate Screen Buffer.  This may be disabled by the titeInhibit resource.', async () => {
+        await ctx.proxy.write('\x1b[?1047h');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'alternate');
+        await ctx.proxy.write('\x1b[?1047l');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
       });
-      test.skip('Ps = 1 0 4 8 - Restore cursor as in DECRC, xterm.  This may be disabled by the titeInhibit resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 8 - Restore cursor as in DECRC, xterm.  This may be disabled by the titeInhibit resource.', async () => {
+        await ctx.proxy.write('\x1b[4;6H');
+        await ctx.proxy.write('\x1b[?1048h');
+        await ctx.proxy.write('\x1b[1;1H');
+        await ctx.proxy.write('\x1b[?1048l');
+        await pollFor(ctx.page, () => getCursor(), { col: 5, row: 3 });
       });
-      test.skip('Ps = 1 0 4 9 - Use Normal Screen Buffer and restore cursor as in DECRC, xterm.  This may be disabled by the titeInhibit resource.  This combines the effects of the 1 0 4 7  and 1 0 4 8  modes.  Use this with terminfo-based applications rather than the 4 7  mode.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 9 - Use Normal Screen Buffer and restore cursor as in DECRC, xterm.  This may be disabled by the titeInhibit resource.  This combines the effects of the 1 0 4 7  and 1 0 4 8  modes.  Use this with terminfo-based applications rather than the 4 7  mode.', async () => {
+        await ctx.proxy.write('\x1b[3;4H');
+        await ctx.proxy.write('\x1b[?1048h');
+        await ctx.proxy.write('\x1b[?47h');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'alternate');
+        await ctx.proxy.write('\x1b[1;1H');
+        await ctx.proxy.write('\x1b[?1049l');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
+        await pollFor(ctx.page, () => getCursor(), { col: 3, row: 2 });
       });
       test.skip('Ps = 1 0 5 0 - Reset terminfo/termcap function-key mode, xterm.', async () => {
         // TODO: Implement
@@ -834,8 +991,11 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 2 0 0 3 - Disable readline mouse button-3, xterm.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 2 0 0 4 - Reset bracketed paste mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 4 - Reset bracketed paste mode, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?2004h');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).bracketedPasteMode, true);
+        await ctx.proxy.write('\x1b[?2004l');
+        await pollFor(ctx.page, async () => (await ctx.proxy.modes).bracketedPasteMode, false);
       });
       test.skip('Ps = 2 0 0 5 - Disable readline character-quoting, xterm.', async () => {
         // TODO: Implement
@@ -1262,29 +1422,75 @@ test.describe('InputHandler Integration Tests', () => {
     test.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => {
       // TODO: Implement
     });
-    test.skip('CSI ! p - DECSTR: Soft terminal reset, VT220 and up.', () => {
-      // TODO: Implement
+    test('CSI ! p - DECSTR: Soft terminal reset, VT220 and up.', async () => {
+      const rows = await ctx.proxy.rows;
+      await ctx.proxy.write('\x1b[4h\x1b[?6h\x1b[3;5r');
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).insertMode, true);
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).originMode, true);
+      await ctx.proxy.write('\x1b[!p');
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).insertMode, false);
+      await pollFor(ctx.page, async () => (await ctx.proxy.modes).originMode, false);
+      await pollFor(ctx.page, () => ctx.page.evaluate(`({ top: window.term._core._bufferService.buffer.scrollTop, bottom: window.term._core._bufferService.buffer.scrollBottom })`), { top: 0, bottom: rows - 1 });
     });
     test.skip('CSI Pl ; Pc " p - DECSCL: Set conformance level, VT220 and up.', () => {
       // TODO: Implement
     });
-    test.skip('CSI Ps $ p - DECRQM: Request ANSI mode', () => {
-      // TODO: Implement
+    test('CSI Ps $ p - DECRQM: Request ANSI mode', async () => {
+      await ctx.proxy.write('\x1b[4h');
+      recordedData.length = 0;
+      await ctx.proxy.write('\x1b[4$p');
+      deepStrictEqual(recordedData, ['\x1b[4;1$y']);
+      await ctx.proxy.write('\x1b[4l');
+      recordedData.length = 0;
+      await ctx.proxy.write('\x1b[4$p');
+      deepStrictEqual(recordedData, ['\x1b[4;2$y']);
+      await ctx.proxy.write('\x1b[20h');
+      recordedData.length = 0;
+      await ctx.proxy.write('\x1b[20$p');
+      deepStrictEqual(recordedData, ['\x1b[20;1$y']);
+      await ctx.proxy.write('\x1b[20l');
     });
-    test.skip('CSI ? Ps $ p - Request DEC private mode (DECRQM).', () => {
-      // TODO: Implement
+    test('CSI ? Ps $ p - Request DEC private mode (DECRQM).', async () => {
+      await ctx.proxy.write('\x1b[?1h');
+      recordedData.length = 0;
+      await ctx.proxy.write('\x1b[?1$p');
+      deepStrictEqual(recordedData, ['\x1b[?1;1$y']);
+      await ctx.proxy.write('\x1b[?1l');
+      recordedData.length = 0;
+      await ctx.proxy.write('\x1b[?1$p');
+      deepStrictEqual(recordedData, ['\x1b[?1;2$y']);
     });
     test.skip('CSI [Pm] # p - Push video attributes onto stack (XTPUSHSGR), xterm.  This is an alias for CSI # { , used to work around language limitations of C#.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI > Ps q - Report xterm name and version (XTVERSION).', async () => {
-      // TODO: Implement
+    test('CSI > Ps q - Report xterm name and version (XTVERSION).', async () => {
+      await ctx.proxy.write('\x1b[>q');
+      ok(recordedData.length === 1);
+      ok(recordedData[0].startsWith('\x1bP>|xterm.js('));
+      ok(recordedData[0].endsWith('\x1b\\'));
     });
     test.skip('CSI Ps q - Load LEDs (DECLL), VT100.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI Ps SP q - Set cursor style (DECSCUSR), VT520.', async () => {
-      // TODO: Implement
+    test('CSI Ps SP q - Set cursor style (DECSCUSR), VT520.', async () => {
+      const getCursorMode = async () => ctx.proxy.core.evaluate(([core]) => {
+        const modes = core.coreService.decPrivateModes;
+        return { style: modes.cursorStyle, blink: modes.cursorBlink };
+      });
+      await ctx.proxy.write('\x1b[1 q');
+      deepStrictEqual(await getCursorMode(), { style: 'block', blink: true });
+      await ctx.proxy.write('\x1b[2 q');
+      deepStrictEqual(await getCursorMode(), { style: 'block', blink: false });
+      await ctx.proxy.write('\x1b[3 q');
+      deepStrictEqual(await getCursorMode(), { style: 'underline', blink: true });
+      await ctx.proxy.write('\x1b[4 q');
+      deepStrictEqual(await getCursorMode(), { style: 'underline', blink: false });
+      await ctx.proxy.write('\x1b[5 q');
+      deepStrictEqual(await getCursorMode(), { style: 'bar', blink: true });
+      await ctx.proxy.write('\x1b[6 q');
+      deepStrictEqual(await getCursorMode(), { style: 'bar', blink: false });
+      await ctx.proxy.write('\x1b[0 q');
+      deepStrictEqual(await getCursorMode(), { style: undefined, blink: undefined });
     });
     test.skip('CSI Ps " q - Select character protection attribute (DECSCA), VT220.', async () => {
       // TODO: Implement
@@ -1292,8 +1498,12 @@ test.describe('InputHandler Integration Tests', () => {
     test.skip('CSI # q - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI Ps ; Ps r - Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.', async () => {
-      // TODO: Implement
+    test('CSI Ps ; Ps r - Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.', async () => {
+      const rows = await ctx.proxy.rows;
+      await ctx.proxy.write('\x1b[2;4r');
+      await pollFor(ctx.page, () => ctx.page.evaluate(`({ top: window.term._core._bufferService.buffer.scrollTop, bottom: window.term._core._bufferService.buffer.scrollBottom })`), { top: 1, bottom: 3 });
+      await ctx.proxy.write('\x1b[r');
+      await pollFor(ctx.page, () => ctx.page.evaluate(`({ top: window.term._core._bufferService.buffer.scrollTop, bottom: window.term._core._bufferService.buffer.scrollBottom })`), { top: 0, bottom: rows - 1 });
     });
     test.skip('CSI ? Pm r - Restore DEC Private Mode Values (XTRESTORE), xterm.', async () => {
       // TODO: Implement
@@ -1301,8 +1511,12 @@ test.describe('InputHandler Integration Tests', () => {
     test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ r - Change Attributes in Rectangular Area (DECCARA), VT400 and up.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI s - Save cursor, available only when DECLRMM is disabled (SCOSC, also ANSI.SYS).', async () => {
-      // TODO: Implement
+    test('CSI s - Save cursor, available only when DECLRMM is disabled (SCOSC, also ANSI.SYS).', async () => {
+      await ctx.proxy.write('\x1b[3;4H');
+      await ctx.proxy.write('\x1b[s');
+      await ctx.proxy.write('\x1b[1;1H');
+      await ctx.proxy.write('\x1b[u');
+      await pollFor(ctx.page, () => getCursor(), { col: 3, row: 2 });
     });
     test.skip('CSI Pl ; Pr s - Set left and right margins (DECSLRM), VT420 and up.', async () => {
       // TODO: Implement
@@ -1322,8 +1536,12 @@ test.describe('InputHandler Integration Tests', () => {
     test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ t - Reverse Attributes in Rectangular Area (DECRARA), VT400 and up.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI u - Restore cursor (SCORC, also ANSI.SYS).', async () => {
-      // TODO: Implement
+    test('CSI u - Restore cursor (SCORC, also ANSI.SYS).', async () => {
+      await ctx.proxy.write('\x1b[4;6H');
+      await ctx.proxy.write('\x1b[s');
+      await ctx.proxy.write('\x1b[1;1H');
+      await ctx.proxy.write('\x1b[u');
+      await pollFor(ctx.page, () => getCursor(), { col: 5, row: 3 });
     });
     test.skip('CSI Ps SP u - Set margin-bell volume (DECSMBV), VT520.', async () => {
       // TODO: Implement
@@ -1382,14 +1600,22 @@ test.describe('InputHandler Integration Tests', () => {
     test.skip('CSI # } - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI Ps \' } - Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.', async () => {
-      // TODO: Implement
+    test('CSI Ps \' } - Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.', async () => {
+      await ctx.proxy.resize(5, 5);
+      await ctx.proxy.write('12345'.repeat(6));
+      await ctx.proxy.write('\x1b[3;3H');
+      await ctx.proxy.write('\x1b[\'}');
+      await pollFor(ctx.page, () => getLinesAsArray(6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
     });
     test.skip('CSI Ps $ } - Select active status display (DECSASD), VT320 and up.', async () => {
       // TODO: Implement
     });
-    test.skip('CSI Ps \' ~ - Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.', async () => {
-      // TODO: Implement
+    test('CSI Ps \' ~ - Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.', async () => {
+      await ctx.proxy.resize(5, 5);
+      await ctx.proxy.write('12345'.repeat(6));
+      await ctx.proxy.write('\x1b[3;3H');
+      await ctx.proxy.write('\x1b[\'~');
+      await pollFor(ctx.page, () => getLinesAsArray(6), ['12345', '1245', '1245', '1245', '1245', '1245']);
     });
     test.skip('CSI Ps $ ~ - Select status line type (DECSSDT), VT320 and up.', async () => {
       // TODO: Implement
@@ -1594,6 +1820,21 @@ async function simulatePaste(text: string): Promise {
   return result;
 }
 
+async function dragSelection(): Promise {
+  await ctx.proxy.clearSelection();
+  const coords: { left: number, top: number, bottom: number, right: number } = await ctx.page.evaluate(`
+    (function() {
+      const rect = window.term.element.getBoundingClientRect();
+      return { left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right };
+    })();
+  `);
+  await ctx.page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2);
+  await ctx.page.mouse.down();
+  await ctx.page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4);
+  await ctx.page.mouse.up();
+  return (await ctx.proxy.getSelection()).length;
+}
+
 async function getCursor(): Promise<{ col: number, row: number }> {
   return {
     col: await ctx.proxy.buffer.active.cursorX,

From 5e532c13c65fe69e446ae70c6cb75d1b57d202ba Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 20:57:51 -0800
Subject: [PATCH 36/49] Enable linting on test/ folder

---
 package.json                           |  4 ++--
 test/playwright/SharedRendererTests.ts |  6 ++----
 test/playwright/TestUtils.ts           | 28 ++++++++++----------------
 3 files changed, 15 insertions(+), 23 deletions(-)

diff --git a/package.json b/package.json
index bc66fbd4..407a2c29 100644
--- a/package.json
+++ b/package.json
@@ -47,10 +47,10 @@
     "esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
     "test": "npm run test-unit",
     "posttest": "npm run lint",
-    "lint": "eslint --max-warnings 0 src/ addons/ demo/",
+    "lint": "eslint --max-warnings 0 src/ addons/ demo/ test/",
     "lint-changes": "node ./bin/lint_changes.js",
     "lint-changes-fix": "node ./bin/lint_changes.js --fix",
-    "lint-fix": "eslint --fix src/ addons/ demo/",
+    "lint-fix": "eslint --fix src/ addons/ demo/ test/",
     "lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/",
     "test-unit": "node ./bin/test_unit.js",
     "test-unit-slow-tests": "npm run test-unit | grep \"ms)\"",
diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts
index 138fba70..8f0e227d 100644
--- a/test/playwright/SharedRendererTests.ts
+++ b/test/playwright/SharedRendererTests.ts
@@ -1012,7 +1012,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
       await ctx.value.proxy.writeln('\x1b[31;42;7m\u{E0B4} red fg green bg inverse\x1b[0m');
       await ctx.value.proxy.writeln('\x1b[32;41;7m\u{E0B4} green fg red bg inverse\x1b[0m');
       await ctx.value.proxy.selectAll();
-      await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255,255,255,255]);
+      await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 255, 255, 255]);
       await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [230, 128, 128, 255]);
       await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [128, 230, 128, 255]);
       await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 4), [128, 230, 128, 255]);
@@ -1397,9 +1397,7 @@ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestCont
  * @param row The 1-based row index to get the color for.
  */
 async function getCellColor(ctx: ITestContext, col: number, row: number, position: CellColorPosition = CellColorPosition.CENTER): Promise<[red: number, green: number, blue: number, alpha: number]> {
-  if (!frameDetails) {
-    frameDetails = await getFrameDetails(ctx);
-  }
+  frameDetails ??= await getFrameDetails(ctx);
   switch (position) {
     case CellColorPosition.CENTER:
       return getCellColorInner(frameDetails, col, row);
diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts
index 3995419f..59bcc1e8 100644
--- a/test/playwright/TestUtils.ts
+++ b/test/playwright/TestUtils.ts
@@ -46,19 +46,17 @@ class EventEmitter {
   private _disposed: boolean = false;
 
   public get event(): IEvent {
-    if (!this._event) {
-      this._event = (listener: (arg1: T, arg2: U) => any) => {
-        this._listeners.add(listener);
-        const disposable = {
-          dispose: () => {
-            if (!this._disposed) {
-              this._listeners.delete(listener);
-            }
+    this._event ??= (listener: (arg1: T, arg2: U) => any) => {
+      this._listeners.add(listener);
+      const disposable = {
+        dispose: () => {
+          if (!this._disposed) {
+            this._listeners.delete(listener);
           }
-        };
-        return disposable;
+        }
       };
-    }
+      return disposable;
+    };
     return this._event;
   }
 
@@ -525,9 +523,7 @@ interface IPollForOptions {
 }
 
 export async function pollFor(page: playwright.Page, evalOrFn: string | (() => MaybeAsync), val: T, preFn?: () => Promise, options?: IPollForOptions): Promise {
-  if (!options) {
-    options = {};
-  }
+  options ??= {};
   options.stack ??= new Error().stack;
   if (preFn) {
     await preFn();
@@ -551,9 +547,7 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() =
   }
 
   if (!equalityCheck) {
-    if (options.maxDuration === undefined) {
-      options.maxDuration = 2000;
-    }
+    options.maxDuration ??= 2000;
     if (options.maxDuration <= 0) {
       deepStrictEqual(result, val, ([
         `pollFor max duration exceeded.`,

From 36baf70beafa1554695140962383268a4a981aa8 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 21:05:36 -0800
Subject: [PATCH 37/49] Unskip supported InputHandler tests

---
 test/playwright/InputHandler.test.ts | 66 ++++++++++++++++++++++------
 1 file changed, 52 insertions(+), 14 deletions(-)

diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts
index 55b63dc9..ea342ebc 100644
--- a/test/playwright/InputHandler.test.ts
+++ b/test/playwright/InputHandler.test.ts
@@ -316,14 +316,16 @@ test.describe('InputHandler Integration Tests', () => {
       await ctx.proxy.write('abcdefg\x1b[3D\x1b[10b#\x1b[3b');
       await pollFor(ctx.page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']);
     });
-    test.skip('CSI Ps c - ', async () => {
-      // TODO: Implement
+    test('CSI Ps c - ', async () => {
+      await ctx.proxy.write('\x1b[c');
+      await pollFor(ctx.page, () => recordedData, ['\x1b[?1;2c']);
     });
     test.skip('CSI = Ps c - ', async () => {
       // TODO: Implement
     });
-    test.skip('CSI > Ps c - ', async () => {
-      // TODO: Implement
+    test('CSI > Ps c - ', async () => {
+      await ctx.proxy.write('\x1b[>c');
+      await pollFor(ctx.page, () => recordedData, ['\x1b[>0;276;0c']);
     });
     test('CSI Ps d - VPA: Line Position Absolute [row] (default = [1,column])', async () => {
       // Default
@@ -425,8 +427,14 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 0 - Show toolbar (rxvt)', async () => {
       // TODO: Implement
       });
-      test.skip('Ps = 1 2 - Start blinking cursor (AT&T 610)', async () => {
-      // TODO: Implement
+      test('Ps = 1 2 - Start blinking cursor (AT&T 610)', async () => {
+        const previousQuirks = await ctx.proxy.getOption('quirks');
+        const previousCursorBlink = await ctx.proxy.getOption('cursorBlink');
+        await ctx.proxy.setOption('quirks', { ...(previousQuirks ?? {}), allowSetCursorBlink: true });
+        await ctx.proxy.write('\x1b[?12h');
+        await pollFor(ctx.page, async () => await ctx.proxy.getOption('cursorBlink'), true);
+        await ctx.proxy.setOption('quirks', previousQuirks);
+        await ctx.proxy.setOption('cursorBlink', previousCursorBlink);
       });
       test.skip('Ps = 1 3 - Start blinking cursor (set only via resource or menu)', async () => {
       // TODO: Implement
@@ -655,8 +663,18 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?1048l');
         await pollFor(ctx.page, () => getCursor(), { col: 4, row: 3 });
       });
-      test.skip('Ps = 1 0 4 9 - Save cursor as in DECSC, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 9 - Save cursor as in DECSC, xterm', async () => {
+        await ctx.proxy.write('main');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
+        await ctx.proxy.write('\x1b[4;6H');
+        await ctx.proxy.write('\x1b[?1049h');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'alternate');
+        await ctx.proxy.write('\x1b[Halt');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['alt']);
+        await ctx.proxy.write('\x1b[?1049l');
+        await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
+        await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
+        await pollFor(ctx.page, () => getCursor(), { col: 5, row: 3 });
       });
       test.skip('Ps = 1 0 5 0 - Set terminfo/termcap function-key mode, xterm', async () => {
       // TODO: Implement
@@ -727,8 +745,15 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 2 - Designate VT52 mode (DECANM), VT100.', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 3 - 80 Column Mode (DECCOLM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 3 - 80 Column Mode (DECCOLM), VT100.', async () => {
+        const windowOptions = await ctx.proxy.getOption('windowOptions');
+        await ctx.proxy.setOption('windowOptions', { ...windowOptions, setWinLines: true });
+        await ctx.proxy.write('\x1b[?3h');
+        await pollFor(ctx.page, async () => await ctx.proxy.cols, 132);
+        await ctx.proxy.write('\x1b[?3l');
+        await pollFor(ctx.page, async () => await ctx.proxy.cols, 80);
+        await ctx.proxy.resize(80, 24);
+        await ctx.proxy.setOption('windowOptions', windowOptions);
       });
       test.skip('Ps = 4 - Jump (Fast) Scroll (DECSCLM), VT100.', async () => {
         // TODO: Implement
@@ -770,8 +795,15 @@ test.describe('InputHandler Integration Tests', () => {
       test.skip('Ps = 1 0 - Hide toolbar (rxvt).', async () => {
         // TODO: Implement
       });
-      test.skip('Ps = 1 2 - Stop blinking cursor (AT&T 610).', async () => {
-        // TODO: Implement
+      test('Ps = 1 2 - Stop blinking cursor (AT&T 610).', async () => {
+        const previousQuirks = await ctx.proxy.getOption('quirks');
+        const previousCursorBlink = await ctx.proxy.getOption('cursorBlink');
+        await ctx.proxy.setOption('quirks', { ...(previousQuirks ?? {}), allowSetCursorBlink: true });
+        await ctx.proxy.setOption('cursorBlink', true);
+        await ctx.proxy.write('\x1b[?12l');
+        await pollFor(ctx.page, async () => await ctx.proxy.getOption('cursorBlink'), false);
+        await ctx.proxy.setOption('quirks', previousQuirks);
+        await ctx.proxy.setOption('cursorBlink', previousCursorBlink);
       });
       test.skip('Ps = 1 3 - Disable blinking cursor (reset only via resource or menu).', async () => {
         // TODO: Implement
@@ -1492,8 +1524,14 @@ test.describe('InputHandler Integration Tests', () => {
       await ctx.proxy.write('\x1b[0 q');
       deepStrictEqual(await getCursorMode(), { style: undefined, blink: undefined });
     });
-    test.skip('CSI Ps " q - Select character protection attribute (DECSCA), VT220.', async () => {
-      // TODO: Implement
+    test('CSI Ps " q - Select character protection attribute (DECSCA), VT220.', async () => {
+      await ctx.proxy.write('\x1b[1"q');
+      await ctx.proxy.write('PROT');
+      await ctx.proxy.write('\x1b[2"q');
+      await ctx.proxy.write('open');
+      await pollFor(ctx.page, () => getLinesAsArray(1), ['PROTopen']);
+      await ctx.proxy.write('\x1b[1;1H\x1b[?2K');
+      await pollFor(ctx.page, () => getLinesAsArray(1), ['PROT']);
     });
     test.skip('CSI # q - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
       // TODO: Implement

From dc487dfd6f5e05b53fc0bd43bda4e82ff9b37597 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 22:01:35 -0800
Subject: [PATCH 38/49] Assert no mode changes and unskip

---
 test/playwright/InputHandler.test.ts | 452 ++++++++++++++-------------
 1 file changed, 239 insertions(+), 213 deletions(-)

diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts
index ea342ebc..a62d1fbc 100644
--- a/test/playwright/InputHandler.test.ts
+++ b/test/playwright/InputHandler.test.ts
@@ -227,9 +227,9 @@ test.describe('InputHandler Integration Tests', () => {
       await ctx.proxy.write('\x1b[2S');
       await pollFor(ctx.page, () => getLinesAsArray(5), ['3', '4', '5', '', '']);
     });
-    test.skip('CSI ? Pi ; Pa ; Pv S - XTSMGRAPHICS: Set or request graphics attribute, xterm', async () => {
-      // TODO: Implement
-    });
+    // This is intentionally not implemented here are image support lives within an addon
+    // test.skip('CSI ? Pi ; Pa ; Pv S - XTSMGRAPHICS: Set or request graphics attribute, xterm', async () => {
+    // });
     test('CSI Ps T - SD: Scroll down Ps lines (default = 1), VT420', async () => {
       await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5');
       await pollFor(ctx.page, () => getLinesAsArray(5), ['1', '2', '3', '4', '5']);
@@ -386,11 +386,11 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.resize(80, 24);
         await ctx.proxy.setOption('windowOptions', windowOptions);
       });
-      test.skip('Ps = 4 - Smooth (Slow) Scroll (DECSCLM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 4 - Smooth (Slow) Scroll (DECSCLM), VT100', async () => {
+        await assertNoModeChange('\x1b[?4h');
       });
-      test.skip('Ps = 5 - Reverse Video (DECSCNM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 5 - Reverse Video (DECSCNM), VT100', async () => {
+        await assertNoModeChange('\x1b[?5h');
       });
       test('Ps = 6 - Origin Mode (DECOM), VT100', async () => {
         await ctx.proxy.write('\x1b[?6h');
@@ -413,8 +413,8 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, () => getLinesAsArray(1), ['1234X']);
         await ctx.proxy.resize(80, 24);
       });
-      test.skip('Ps = 8 - Auto-Repeat Keys (DECARM), VT100', async () => {
-      // TODO: Implement
+      test('Ps = 8 - Auto-Repeat Keys (DECARM), VT100', async () => {
+        await assertNoModeChange('\x1b[?8h');
       });
       test('Ps = 9 - Send Mouse X & Y on button press', async () => {
         const selectionBefore = await dragSelection();
@@ -424,8 +424,8 @@ test.describe('InputHandler Integration Tests', () => {
         const selectionAfter = await dragSelection();
         ok(selectionAfter === 0);
       });
-      test.skip('Ps = 1 0 - Show toolbar (rxvt)', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 - Show toolbar (rxvt)', async () => {
+        await assertNoModeChange('\x1b[?10h');
       });
       test('Ps = 1 2 - Start blinking cursor (AT&T 610)', async () => {
         const previousQuirks = await ctx.proxy.getOption('quirks');
@@ -436,17 +436,17 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.setOption('quirks', previousQuirks);
         await ctx.proxy.setOption('cursorBlink', previousCursorBlink);
       });
-      test.skip('Ps = 1 3 - Start blinking cursor (set only via resource or menu)', async () => {
-      // TODO: Implement
+      test('Ps = 1 3 - Start blinking cursor (set only via resource or menu)', async () => {
+        await assertNoModeChange('\x1b[?13h');
       });
-      test.skip('Ps = 1 4 - Enable XOR of blinking cursor control sequence and menu', async () => {
-      // TODO: Implement
+      test('Ps = 1 4 - Enable XOR of blinking cursor control sequence and menu', async () => {
+        await assertNoModeChange('\x1b[?14h');
       });
-      test.skip('Ps = 1 8 - Print Form Feed (DECPFF), VT220', async () => {
-      // TODO: Implement
+      test('Ps = 1 8 - Print Form Feed (DECPFF), VT220', async () => {
+        await assertNoModeChange('\x1b[?18h');
       });
-      test.skip('Ps = 1 9 - Set print extent to full screen (DECPEX), VT220', async () => {
-      // TODO: Implement
+      test('Ps = 1 9 - Set print extent to full screen (DECPEX), VT220', async () => {
+        await assertNoModeChange('\x1b[?19h');
       });
       test('Ps = 2 5 - Show cursor (DECTCEM), VT220', async () => {
         await ctx.proxy.write('\x1b[?25l');
@@ -454,32 +454,32 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?25h');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).showCursor, true);
       });
-      test.skip('Ps = 3 0 - Show scrollbar (rxvt)', async () => {
-      // TODO: Implement
+      test('Ps = 3 0 - Show scrollbar (rxvt)', async () => {
+        await assertNoModeChange('\x1b[?30h');
       });
-      test.skip('Ps = 3 5 - Enable font-shifting functions (rxvt)', async () => {
-      // TODO: Implement
+      test('Ps = 3 5 - Enable font-shifting functions (rxvt)', async () => {
+        await assertNoModeChange('\x1b[?35h');
       });
-      test.skip('Ps = 3 8 - Enter Tektronix mode (DECTEK), VT240, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 3 8 - Enter Tektronix mode (DECTEK), VT240, xterm', async () => {
+        await assertNoModeChange('\x1b[?38h');
       });
-      test.skip('Ps = 4 0 - Allow 80 ⇒  132 mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 4 0 - Allow 80 ⇒  132 mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?40h');
       });
-      test.skip('Ps = 4 1 - more(1) fix (see curses resource)', async () => {
-      // TODO: Implement
+      test('Ps = 4 1 - more(1) fix (see curses resource)', async () => {
+        await assertNoModeChange('\x1b[?41h');
       });
-      test.skip('Ps = 4 2 - Enable National Replacement Character sets (DECNRCM), VT220', async () => {
-      // TODO: Implement
+      test('Ps = 4 2 - Enable National Replacement Character sets (DECNRCM), VT220', async () => {
+        await assertNoModeChange('\x1b[?42h');
       });
-      test.skip('Ps = 4 3 - Enable Graphic Expanded Print Mode (DECGEPM), VT340', async () => {
-      // TODO: Implement
+      test('Ps = 4 3 - Enable Graphic Expanded Print Mode (DECGEPM), VT340', async () => {
+        await assertNoModeChange('\x1b[?43h');
       });
-      test.skip('Ps = 4 4 - Turn on margin bell, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 4 4 - Turn on margin bell, xterm', async () => {
+        await assertNoModeChange('\x1b[?44h');
       });
-      test.skip('Ps = 4 4 - Enable Graphic Print Color Mode (DECGPCM), VT340', async () => {
-      // TODO: Implement
+      test('Ps = 4 4 - Enable Graphic Print Color Mode (DECGPCM), VT340', async () => {
+        await assertNoModeChange('\x1b[?44h');
       });
       test('Ps = 4 5 - Reverse-wraparound mode (XTREVWRAP), xterm', async () => {
         await ctx.proxy.write('\x1b[?45h');
@@ -492,13 +492,12 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.resize(80, 24);
       });
       test.skip('Ps = 4 5 - Enable Graphic Print Color Syntax (DECGPCS), VT340', async () => {
-      // TODO: Implement
       });
-      test.skip('Ps = 4 6 - Start logging (XTLOGGING), xterm', async () => {
-      // TODO: Implement
+      test('Ps = 4 6 - Start logging (XTLOGGING), xterm', async () => {
+        await assertNoModeChange('\x1b[?46h');
       });
-      test.skip('Ps = 4 6 - Graphic Print Background Mode, VT340', async () => {
-      // TODO: Implement
+      test('Ps = 4 6 - Graphic Print Background Mode, VT340', async () => {
+        await assertNoModeChange('\x1b[?46h');
       });
       test('Ps = 4 7 - Use Alternate Screen Buffer, xterm', async () => {
         await ctx.proxy.write('main');
@@ -513,23 +512,21 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
       });
       test.skip('Ps = 4 7 - Enable Graphic Rotated Print Mode (DECGRPM), VT340', async () => {
-      // TODO: Implement
       });
       test('Ps = 6 6 - Application keypad mode (DECNKM), VT320', async () => {
         await ctx.proxy.write('\x1b[?66h');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationKeypadMode, true);
       });
-      test.skip('Ps = 6 7 - Backarrow key sends backspace (DECBKM), VT340, VT420', async () => {
-      // TODO: Implement
+      test('Ps = 6 7 - Backarrow key sends backspace (DECBKM), VT340, VT420', async () => {
+        await assertNoModeChange('\x1b[?67h');
       });
-      test.skip('Ps = 6 9 - Enable left and right margin mode (DECLRMM), VT420 and up', async () => {
-      // TODO: Implement
+      test('Ps = 6 9 - Enable left and right margin mode (DECLRMM), VT420 and up', async () => {
+        await assertNoModeChange('\x1b[?69h');
       });
-      test.skip('Ps = 8 0 - Enable Sixel Display Mode (DECSDM), VT330, VT340, VT382', async () => {
-      // TODO: Implement
-      });
-      test.skip('Ps = 9 5 - Do not clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up', async () => {
-      // TODO: Implement
+      // test.skip('Ps = 8 0 - Enable Sixel Display Mode (DECSDM), VT330, VT340, VT382', async () => {
+      // });
+      test('Ps = 9 5 - Do not clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up', async () => {
+        await assertNoModeChange('\x1b[?95h');
       });
       test('Ps = 1 0 0 0 - Send Mouse X & Y on button press and release', async () => {
         const selectionBefore = await dragSelection();
@@ -539,8 +536,8 @@ test.describe('InputHandler Integration Tests', () => {
         const selectionAfter = await dragSelection();
         ok(selectionAfter === 0);
       });
-      test.skip('Ps = 1 0 0 1 - Use Hilite Mouse Tracking, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 1 - Use Hilite Mouse Tracking, xterm', async () => {
+        await assertNoModeChange('\x1b[?1001h');
       });
       test('Ps = 1 0 0 2 - Use Cell Motion Mouse Tracking, xterm', async () => {
         const selectionBefore = await dragSelection();
@@ -585,64 +582,70 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.blur();
         await pollFor(ctx.page, () => recordedData, ['\x1b[I', '\x1b[O']);
       });
-      test.skip('Ps = 1 0 0 5 - Enable UTF-8 Mouse Mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 5 - Enable UTF-8 Mouse Mode, xterm', async () => {
+        await ctx.proxy.write('\x1b[?1006h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
+        await ctx.proxy.write('\x1b[?1005h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
       });
       test('Ps = 1 0 0 6 - Enable SGR Mouse Mode, xterm', async () => {
         await ctx.proxy.write('\x1b[?1006h');
         await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
       });
-      test.skip('Ps = 1 0 0 7 - Enable Alternate Scroll Mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 0 7 - Enable Alternate Scroll Mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?1007h');
       });
-      test.skip('Ps = 1 0 1 0 - Scroll to bottom on tty output (rxvt)', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 1 0 - Scroll to bottom on tty output (rxvt)', async () => {
+        await assertNoModeChange('\x1b[?1010h');
       });
-      test.skip('Ps = 1 0 1 1 - Scroll to bottom on key press (rxvt)', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 1 1 - Scroll to bottom on key press (rxvt)', async () => {
+        await assertNoModeChange('\x1b[?1011h');
       });
-      test.skip('Ps = 1 0 1 5 - Enable urxvt Mouse Mode', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 1 5 - Enable urxvt Mouse Mode', async () => {
+        await ctx.proxy.write('\x1b[?1006h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
+        await ctx.proxy.write('\x1b[?1015h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
       });
       test('Ps = 1 0 1 6 - Enable SGR Mouse PixelMode, xterm', async () => {
         await ctx.proxy.write('\x1b[?1016h');
         await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR_PIXELS');
       });
-      test.skip('Ps = 1 0 3 4 - Interpret "meta" key, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 3 4 - Interpret "meta" key, xterm', async () => {
+        await assertNoModeChange('\x1b[?1034h');
       });
-      test.skip('Ps = 1 0 3 5 - Enable special modifiers for Alt and NumLock keys, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 3 5 - Enable special modifiers for Alt and NumLock keys, xterm', async () => {
+        await assertNoModeChange('\x1b[?1035h');
       });
-      test.skip('Ps = 1 0 3 6 - Send ESC   when Meta modifies a key, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 3 6 - Send ESC   when Meta modifies a key, xterm', async () => {
+        await assertNoModeChange('\x1b[?1036h');
       });
-      test.skip('Ps = 1 0 3 7 - Send DEL from the editing-keypad Delete key, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 3 7 - Send DEL from the editing-keypad Delete key, xterm', async () => {
+        await assertNoModeChange('\x1b[?1037h');
       });
-      test.skip('Ps = 1 0 3 9 - Send ESC  when Alt modifies a key, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 3 9 - Send ESC  when Alt modifies a key, xterm', async () => {
+        await assertNoModeChange('\x1b[?1039h');
       });
-      test.skip('Ps = 1 0 4 0 - Keep selection even if not highlighted, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 0 - Keep selection even if not highlighted, xterm', async () => {
+        await assertNoModeChange('\x1b[?1040h');
       });
-      test.skip('Ps = 1 0 4 1 - Use the CLIPBOARD selection, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 1 - Use the CLIPBOARD selection, xterm', async () => {
+        await assertNoModeChange('\x1b[?1041h');
       });
-      test.skip('Ps = 1 0 4 2 - Enable Urgency window manager hint when Control-G is received, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 2 - Enable Urgency window manager hint when Control-G is received, xterm', async () => {
+        await assertNoModeChange('\x1b[?1042h');
       });
-      test.skip('Ps = 1 0 4 3 - Enable raising of the window when Control-G is received, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 3 - Enable raising of the window when Control-G is received, xterm', async () => {
+        await assertNoModeChange('\x1b[?1043h');
       });
-      test.skip('Ps = 1 0 4 4 - Reuse the most recent data copied to CLIPBOARD, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 4 - Reuse the most recent data copied to CLIPBOARD, xterm', async () => {
+        await assertNoModeChange('\x1b[?1044h');
       });
-      test.skip('Ps = 1 0 4 5 - XTREVWRAP2: Extended Reverse-wraparound mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 5 - XTREVWRAP2: Extended Reverse-wraparound mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?1045h');
       });
-      test.skip('Ps = 1 0 4 6 - Enable switching to/from Alternate Screen Buffer, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 4 6 - Enable switching to/from Alternate Screen Buffer, xterm', async () => {
+        await assertNoModeChange('\x1b[?1046h');
       });
       test('Ps = 1 0 4 7 - Use Alternate Screen Buffer, xterm', async () => {
         await ctx.proxy.write('main');
@@ -676,32 +679,32 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
         await pollFor(ctx.page, () => getCursor(), { col: 5, row: 3 });
       });
-      test.skip('Ps = 1 0 5 0 - Set terminfo/termcap function-key mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 5 0 - Set terminfo/termcap function-key mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?1050h');
       });
-      test.skip('Ps = 1 0 5 1 - Set Sun function-key mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 5 1 - Set Sun function-key mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?1051h');
       });
-      test.skip('Ps = 1 0 5 2 - Set HP function-key mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 5 2 - Set HP function-key mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?1052h');
       });
-      test.skip('Ps = 1 0 5 3 - Set SCO function-key mode, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 5 3 - Set SCO function-key mode, xterm', async () => {
+        await assertNoModeChange('\x1b[?1053h');
       });
-      test.skip('Ps = 1 0 6 0 - Set legacy keyboard emulation, i.e, X11R6, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 6 0 - Set legacy keyboard emulation, i.e, X11R6, xterm', async () => {
+        await assertNoModeChange('\x1b[?1060h');
       });
-      test.skip('Ps = 1 0 6 1 - Set VT220 keyboard emulation, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 1 0 6 1 - Set VT220 keyboard emulation, xterm', async () => {
+        await assertNoModeChange('\x1b[?1061h');
       });
-      test.skip('Ps = 2 0 0 1 - Enable readline mouse button-1, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 2 0 0 1 - Enable readline mouse button-1, xterm', async () => {
+        await assertNoModeChange('\x1b[?2001h');
       });
-      test.skip('Ps = 2 0 0 2 - Enable readline mouse button-2, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 2 0 0 2 - Enable readline mouse button-2, xterm', async () => {
+        await assertNoModeChange('\x1b[?2002h');
       });
-      test.skip('Ps = 2 0 0 3 - Enable readline mouse button-3, xterm', async () => {
-      // TODO: Implement
+      test('Ps = 2 0 0 3 - Enable readline mouse button-3, xterm', async () => {
+        await assertNoModeChange('\x1b[?2003h');
       });
       test('Pm = 2 0 0 4, Set bracketed paste mode', async () => {
         if (ctx.browser.browserType().name() !== 'chromium') {
@@ -714,11 +717,11 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?2004l');
         await pollFor(ctx.page, () => simulatePaste('baz'), 'baz');
       });
-      test.skip('Ps = 2 0 0 5 - Enable readline character-quoting, xterm', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 5 - Enable readline character-quoting, xterm', async () => {
+        await assertNoModeChange('\x1b[?2005h');
       });
-      test.skip('Ps = 2 0 0 6 - Enable readline newline pasting, xterm', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 6 - Enable readline newline pasting, xterm', async () => {
+        await assertNoModeChange('\x1b[?2006h');
       });
     });
     test.skip('CSI Ps i - MC: Media Copy', async () => {
@@ -742,8 +745,8 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?1l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationCursorKeysMode, false);
       });
-      test.skip('Ps = 2 - Designate VT52 mode (DECANM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 2 - Designate VT52 mode (DECANM), VT100.', async () => {
+        await assertNoModeChange('\x1b[?2l');
       });
       test('Ps = 3 - 80 Column Mode (DECCOLM), VT100.', async () => {
         const windowOptions = await ctx.proxy.getOption('windowOptions');
@@ -755,11 +758,11 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.resize(80, 24);
         await ctx.proxy.setOption('windowOptions', windowOptions);
       });
-      test.skip('Ps = 4 - Jump (Fast) Scroll (DECSCLM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 4 - Jump (Fast) Scroll (DECSCLM), VT100.', async () => {
+        await assertNoModeChange('\x1b[?4l');
       });
-      test.skip('Ps = 5 - Normal Video (DECSCNM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 5 - Normal Video (DECSCNM), VT100.', async () => {
+        await assertNoModeChange('\x1b[?5l');
       });
       test('Ps = 6 - Normal Cursor Mode (DECOM), VT100.', async () => {
         await ctx.proxy.write('\x1b[?6h');
@@ -783,8 +786,8 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, () => getLinesAsArray(2), ['12345', 'X']);
         await ctx.proxy.resize(80, 24);
       });
-      test.skip('Ps = 8 - No Auto-Repeat Keys (DECARM), VT100.', async () => {
-        // TODO: Implement
+      test('Ps = 8 - No Auto-Repeat Keys (DECARM), VT100.', async () => {
+        await assertNoModeChange('\x1b[?8l');
       });
       test('Ps = 9 - Don\'t send Mouse X & Y on button press, xterm.', async () => {
         await ctx.proxy.write('\x1b[?9h');
@@ -792,8 +795,8 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?9l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'none');
       });
-      test.skip('Ps = 1 0 - Hide toolbar (rxvt).', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 - Hide toolbar (rxvt).', async () => {
+        await assertNoModeChange('\x1b[?10l');
       });
       test('Ps = 1 2 - Stop blinking cursor (AT&T 610).', async () => {
         const previousQuirks = await ctx.proxy.getOption('quirks');
@@ -805,17 +808,17 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.setOption('quirks', previousQuirks);
         await ctx.proxy.setOption('cursorBlink', previousCursorBlink);
       });
-      test.skip('Ps = 1 3 - Disable blinking cursor (reset only via resource or menu).', async () => {
-        // TODO: Implement
+      test('Ps = 1 3 - Disable blinking cursor (reset only via resource or menu).', async () => {
+        await assertNoModeChange('\x1b[?13l');
       });
-      test.skip('Ps = 1 4 - Disable XOR of blinking cursor control sequence and menu.', async () => {
-        // TODO: Implement
+      test('Ps = 1 4 - Disable XOR of blinking cursor control sequence and menu.', async () => {
+        await assertNoModeChange('\x1b[?14l');
       });
-      test.skip('Ps = 1 8 - Don\'t Print Form Feed (DECPFF), VT220.', async () => {
-        // TODO: Implement
+      test('Ps = 1 8 - Don\'t Print Form Feed (DECPFF), VT220.', async () => {
+        await assertNoModeChange('\x1b[?18l');
       });
-      test.skip('Ps = 1 9 - Limit print to scrolling region (DECPEX), VT220.', async () => {
-        // TODO: Implement
+      test('Ps = 1 9 - Limit print to scrolling region (DECPEX), VT220.', async () => {
+        await assertNoModeChange('\x1b[?19l');
       });
       test('Ps = 2 5 - Hide cursor (DECTCEM), VT220.', async () => {
         await ctx.proxy.write('\x1b[?25h');
@@ -823,29 +826,29 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?25l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).showCursor, false);
       });
-      test.skip('Ps = 3 0 - Don\'t show scrollbar (rxvt).', async () => {
-        // TODO: Implement
+      test('Ps = 3 0 - Don\'t show scrollbar (rxvt).', async () => {
+        await assertNoModeChange('\x1b[?30l');
       });
-      test.skip('Ps = 3 5 - Disable font-shifting functions (rxvt).', async () => {
-        // TODO: Implement
+      test('Ps = 3 5 - Disable font-shifting functions (rxvt).', async () => {
+        await assertNoModeChange('\x1b[?35l');
       });
-      test.skip('Ps = 4 0 - Disallow 80 ⇒  132 mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 4 0 - Disallow 80 ⇒  132 mode, xterm.', async () => {
+        await assertNoModeChange('\x1b[?40l');
       });
-      test.skip('Ps = 4 1 - No more(1) fix (see curses resource).', async () => {
-        // TODO: Implement
+      test('Ps = 4 1 - No more(1) fix (see curses resource).', async () => {
+        await assertNoModeChange('\x1b[?41l');
       });
-      test.skip('Ps = 4 2 - Disable National Replacement Character sets (DECNRCM), VT220.', async () => {
-        // TODO: Implement
+      test('Ps = 4 2 - Disable National Replacement Character sets (DECNRCM), VT220.', async () => {
+        await assertNoModeChange('\x1b[?42l');
       });
-      test.skip('Ps = 4 3 - Disable Graphic Expanded Print Mode (DECGEPM), VT340.', async () => {
-        // TODO: Implement
+      test('Ps = 4 3 - Disable Graphic Expanded Print Mode (DECGEPM), VT340.', async () => {
+        await assertNoModeChange('\x1b[?43l');
       });
-      test.skip('Ps = 4 4 - Turn off margin bell, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 4 4 - Turn off margin bell, xterm.', async () => {
+        await assertNoModeChange('\x1b[?44l');
       });
-      test.skip('Ps = 4 4 - Disable Graphic Print Color Mode (DECGPCM), VT340.', async () => {
-        // TODO: Implement
+      test('Ps = 4 4 - Disable Graphic Print Color Mode (DECGPCM), VT340.', async () => {
+        await assertNoModeChange('\x1b[?44l');
       });
       test('Ps = 4 5 - No Reverse-wraparound mode (XTREVWRAP), xterm.', async () => {
         await ctx.proxy.write('\x1b[?45h');
@@ -854,10 +857,9 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).reverseWraparoundMode, false);
       });
       test.skip('Ps = 4 5 - Disable Graphic Print Color Syntax (DECGPCS), VT340.', async () => {
-        // TODO: Implement
       });
-      test.skip('Ps = 4 6 - Stop logging (XTLOGGING), xterm.  This is normally disabled by a compile-time option.', async () => {
-        // TODO: Implement
+      test('Ps = 4 6 - Stop logging (XTLOGGING), xterm.  This is normally disabled by a compile-time option.', async () => {
+        await assertNoModeChange('\x1b[?46l');
       });
       test('Ps = 4 7 - Use Normal Screen Buffer, xterm.', async () => {
         await ctx.proxy.write('\x1b[?47h');
@@ -866,7 +868,6 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
       });
       test.skip('Ps = 4 7 - Disable Graphic Rotated Print Mode (DECGRPM), VT340.', async () => {
-        // TODO: Implement
       });
       test('Ps = 6 6 - Numeric keypad mode (DECNKM), VT320.', async () => {
         await ctx.proxy.write('\x1b[?66h');
@@ -874,17 +875,17 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?66l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).applicationKeypadMode, false);
       });
-      test.skip('Ps = 6 7 - Backarrow key sends delete (DECBKM), VT340, VT420.  This sets the backarrowKey resource to "false".', async () => {
-        // TODO: Implement
+      test('Ps = 6 7 - Backarrow key sends delete (DECBKM), VT340, VT420.  This sets the backarrowKey resource to "false".', async () => {
+        await assertNoModeChange('\x1b[?67l');
       });
-      test.skip('Ps = 6 9 - Disable left and right margin mode (DECLRMM), VT420 and up.', async () => {
-        // TODO: Implement
+      test('Ps = 6 9 - Disable left and right margin mode (DECLRMM), VT420 and up.', async () => {
+        await assertNoModeChange('\x1b[?69l');
       });
-      test.skip('Ps = 8 0 - Disable Sixel Display Mode (DECSDM), VT330, VT340, VT382.  Turns on "Sixel Scrolling".  See the section Sixel Graphics and mode 8 4 5 2 .', async () => {
-        // TODO: Implement
-      });
-      test.skip('Ps = 9 5 - Clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up.', async () => {
-        // TODO: Implement
+      // This is intentionally not implemented here are image support lives within an addon
+      // test.skip('Ps = 8 0 - Disable Sixel Display Mode (DECSDM), VT330, VT340, VT382.  Turns on "Sixel Scrolling".  See the section Sixel Graphics and mode 8 4 5 2 .', async () => {
+      // });
+      test('Ps = 9 5 - Clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up.', async () => {
+        await assertNoModeChange('\x1b[?95l');
       });
       test('Ps = 1 0 0 0 - Don\'t send Mouse X & Y on button press and release.  See the section Mouse Tracking.', async () => {
         await ctx.proxy.write('\x1b[?1000h');
@@ -892,8 +893,8 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?1000l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).mouseTrackingMode, 'none');
       });
-      test.skip('Ps = 1 0 0 1 - Don\'t use Hilite Mouse Tracking, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 1 - Don\'t use Hilite Mouse Tracking, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1001l');
       });
       test('Ps = 1 0 0 2 - Don\'t use Cell Motion Mouse Tracking, xterm.  See the section Button-event tracking.', async () => {
         await ctx.proxy.write('\x1b[?1002h');
@@ -913,8 +914,11 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?1004l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).sendFocusMode, false);
       });
-      test.skip('Ps = 1 0 0 5 - Disable UTF-8 Mouse Mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 5 - Disable UTF-8 Mouse Mode, xterm.', async () => {
+        await ctx.proxy.write('\x1b[?1006h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
+        await ctx.proxy.write('\x1b[?1005l');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
       });
       test('Ps = 1 0 0 6 - Disable SGR Mouse Mode, xterm.', async () => {
         await ctx.proxy.write('\x1b[?1006h');
@@ -922,17 +926,20 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?1006l');
         await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'DEFAULT');
       });
-      test.skip('Ps = 1 0 0 7 - Disable Alternate Scroll Mode, xterm.  This corresponds to the alternateScroll resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 0 7 - Disable Alternate Scroll Mode, xterm.  This corresponds to the alternateScroll resource.', async () => {
+        await assertNoModeChange('\x1b[?1007l');
       });
-      test.skip('Ps = 1 0 1 0 - Don\'t scroll to bottom on tty output (rxvt).  This sets the scrollTtyOutput resource to "false".', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 1 0 - Don\'t scroll to bottom on tty output (rxvt).  This sets the scrollTtyOutput resource to "false".', async () => {
+        await assertNoModeChange('\x1b[?1010l');
       });
-      test.skip('Ps = 1 0 1 1 - Don\'t scroll to bottom on key press (rxvt). This sets the scrollKey resource to "false".', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 1 1 - Don\'t scroll to bottom on key press (rxvt). This sets the scrollKey resource to "false".', async () => {
+        await assertNoModeChange('\x1b[?1011l');
       });
-      test.skip('Ps = 1 0 1 5 - Disable urxvt Mouse Mode.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 1 5 - Disable urxvt Mouse Mode.', async () => {
+        await ctx.proxy.write('\x1b[?1006h');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
+        await ctx.proxy.write('\x1b[?1015l');
+        await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR');
       });
       test('Ps = 1 0 1 6 - Disable SGR Mouse Pixel-Mode, xterm.', async () => {
         await ctx.proxy.write('\x1b[?1016h');
@@ -940,38 +947,38 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?1016l');
         await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'DEFAULT');
       });
-      test.skip('Ps = 1 0 3 4 - Don\'t interpret "meta" key, xterm.  This disables the eightBitInput resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 3 4 - Don\'t interpret "meta" key, xterm.  This disables the eightBitInput resource.', async () => {
+        await assertNoModeChange('\x1b[?1034l');
       });
-      test.skip('Ps = 1 0 3 5 - Disable special modifiers for Alt and NumLock keys, xterm.  This disables the numLock resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 3 5 - Disable special modifiers for Alt and NumLock keys, xterm.  This disables the numLock resource.', async () => {
+        await assertNoModeChange('\x1b[?1035l');
       });
-      test.skip('Ps = 1 0 3 6 - Don\'t send ESC  when Meta modifies a key, xterm.  This disables the metaSendsEscape resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 3 6 - Don\'t send ESC  when Meta modifies a key, xterm.  This disables the metaSendsEscape resource.', async () => {
+        await assertNoModeChange('\x1b[?1036l');
       });
-      test.skip('Ps = 1 0 3 7 - Send VT220 Remove from the editing-keypad Delete key, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 3 7 - Send VT220 Remove from the editing-keypad Delete key, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1037l');
       });
-      test.skip('Ps = 1 0 3 9 - Don\'t send ESC when Alt modifies a key, xterm.  This disables the altSendsEscape resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 3 9 - Don\'t send ESC when Alt modifies a key, xterm.  This disables the altSendsEscape resource.', async () => {
+        await assertNoModeChange('\x1b[?1039l');
       });
-      test.skip('Ps = 1 0 4 0 - Do not keep selection when not highlighted, xterm.  This disables the keepSelection resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 0 - Do not keep selection when not highlighted, xterm.  This disables the keepSelection resource.', async () => {
+        await assertNoModeChange('\x1b[?1040l');
       });
-      test.skip('Ps = 1 0 4 1 - Use the PRIMARY selection, xterm.  This disables the selectToClipboard resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 1 - Use the PRIMARY selection, xterm.  This disables the selectToClipboard resource.', async () => {
+        await assertNoModeChange('\x1b[?1041l');
       });
-      test.skip('Ps = 1 0 4 2 - Disable Urgency window manager hint when Control-G is received, xterm.  This disables the bellIsUrgent resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 2 - Disable Urgency window manager hint when Control-G is received, xterm.  This disables the bellIsUrgent resource.', async () => {
+        await assertNoModeChange('\x1b[?1042l');
       });
-      test.skip('Ps = 1 0 4 3 - Disable raising of the window when Control- G is received, xterm.  This disables the popOnBell resource.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 3 - Disable raising of the window when Control- G is received, xterm.  This disables the popOnBell resource.', async () => {
+        await assertNoModeChange('\x1b[?1043l');
       });
-      test.skip('Ps = 1 0 4 5 - No Extended Reverse-wraparound mode (XTREVWRAP2), xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 5 - No Extended Reverse-wraparound mode (XTREVWRAP2), xterm.', async () => {
+        await assertNoModeChange('\x1b[?1045l');
       });
-      test.skip('Ps = 1 0 4 6 - Disable switching to/from Alternate Screen Buffer, xterm.  This works for terminfo-based systems, updating the titeInhibit resource.  If currently using the Alternate Screen Buffer, xterm switches to the Normal Screen Buffer.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 4 6 - Disable switching to/from Alternate Screen Buffer, xterm.  This works for terminfo-based systems, updating the titeInhibit resource.  If currently using the Alternate Screen Buffer, xterm switches to the Normal Screen Buffer.', async () => {
+        await assertNoModeChange('\x1b[?1046l');
       });
       test('Ps = 1 0 4 7 - Use Normal Screen Buffer, xterm.  Clear the screen first if in the Alternate Screen Buffer.  This may be disabled by the titeInhibit resource.', async () => {
         await ctx.proxy.write('\x1b[?1047h');
@@ -996,32 +1003,32 @@ test.describe('InputHandler Integration Tests', () => {
         await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
         await pollFor(ctx.page, () => getCursor(), { col: 3, row: 2 });
       });
-      test.skip('Ps = 1 0 5 0 - Reset terminfo/termcap function-key mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 5 0 - Reset terminfo/termcap function-key mode, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1050l');
       });
-      test.skip('Ps = 1 0 5 1 - Reset Sun function-key mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 5 1 - Reset Sun function-key mode, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1051l');
       });
-      test.skip('Ps = 1 0 5 2 - Reset HP function-key mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 5 2 - Reset HP function-key mode, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1052l');
       });
-      test.skip('Ps = 1 0 5 3 - Reset SCO function-key mode, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 5 3 - Reset SCO function-key mode, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1053l');
       });
-      test.skip('Ps = 1 0 6 0 - Reset legacy keyboard emulation, i.e, X11R6, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 6 0 - Reset legacy keyboard emulation, i.e, X11R6, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1060l');
       });
-      test.skip('Ps = 1 0 6 1 - Reset keyboard emulation to Sun/PC style, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 1 0 6 1 - Reset keyboard emulation to Sun/PC style, xterm.', async () => {
+        await assertNoModeChange('\x1b[?1061l');
       });
-      test.skip('Ps = 2 0 0 1 - Disable readline mouse button-1, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 1 - Disable readline mouse button-1, xterm.', async () => {
+        await assertNoModeChange('\x1b[?2001l');
       });
-      test.skip('Ps = 2 0 0 2 - Disable readline mouse button-2, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 2 - Disable readline mouse button-2, xterm.', async () => {
+        await assertNoModeChange('\x1b[?2002l');
       });
-      test.skip('Ps = 2 0 0 3 - Disable readline mouse button-3, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 3 - Disable readline mouse button-3, xterm.', async () => {
+        await assertNoModeChange('\x1b[?2003l');
       });
       test('Ps = 2 0 0 4 - Reset bracketed paste mode, xterm.', async () => {
         await ctx.proxy.write('\x1b[?2004h');
@@ -1029,11 +1036,11 @@ test.describe('InputHandler Integration Tests', () => {
         await ctx.proxy.write('\x1b[?2004l');
         await pollFor(ctx.page, async () => (await ctx.proxy.modes).bracketedPasteMode, false);
       });
-      test.skip('Ps = 2 0 0 5 - Disable readline character-quoting, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 5 - Disable readline character-quoting, xterm.', async () => {
+        await assertNoModeChange('\x1b[?2005l');
       });
-      test.skip('Ps = 2 0 0 6 - Disable readline newline pasting, xterm.', async () => {
-        // TODO: Implement
+      test('Ps = 2 0 0 6 - Disable readline newline pasting, xterm.', async () => {
+        await assertNoModeChange('\x1b[?2006l');
       });
     });
     test.describe('CSI Pm m - SGR: Character Attributes', () => {
@@ -1835,6 +1842,25 @@ test.describe('InputHandler Integration Tests', () => {
 });
 
 
+async function getModeSnapshot(): Promise {
+  return {
+    modes: await ctx.proxy.modes,
+    cursorBlink: await ctx.proxy.getOption('cursorBlink'),
+    cols: await ctx.proxy.cols,
+    rows: await ctx.proxy.rows,
+    bufferType: await ctx.proxy.buffer.active.type,
+    mouseProtocol: await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeProtocol),
+    mouseEncoding: await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding)
+  };
+}
+
+async function assertNoModeChange(sequence: string): Promise {
+  const before = await getModeSnapshot();
+  await ctx.proxy.write(sequence);
+  deepStrictEqual(await getModeSnapshot(), before);
+}
+
+
 async function getLinesAsArray(count: number, start: number = 0): Promise {
   let text = '';
   for (let i = start; i < start + count; i++) {

From 7b417f4585f07df67b369f30c63d811eb869519c Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 22:05:33 -0800
Subject: [PATCH 39/49] win32 input mode: Avoid scrolling to bottom on modifier
 only

Fixes #5612
---
 src/browser/CoreBrowserTerminal.ts | 13 ++++++++---
 src/browser/Terminal.test.ts       | 36 ++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts
index 51d40224..6e0c7ad1 100644
--- a/src/browser/CoreBrowserTerminal.ts
+++ b/src/browser/CoreBrowserTerminal.ts
@@ -1156,9 +1156,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
       this.textarea!.value = '';
     }
 
+    const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(event);
     this._onKey.fire({ key: result.key, domEvent: event });
     this._showCursor();
-    this.coreService.triggerDataEvent(result.key, true);
+    this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
 
     // Cancel events when not in screen reader mode so events don't get bubbled up and handled by
     // other listeners. When screen reader mode is enabled, we don't cancel them (unless ctrl or alt
@@ -1199,7 +1200,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     // Handle key release for Kitty keyboard protocol
     const result = this._keyboardService.evaluateKeyUp(ev);
     if (result?.key) {
-      this.coreService.triggerDataEvent(result.key, true);
+      const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(ev);
+      this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
     }
 
     this.updateCursorStyle(ev);
@@ -1410,5 +1412,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
 function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
   return ev.keyCode === 16 || // Shift
     ev.keyCode === 17 || // Ctrl
-    ev.keyCode === 18; // Alt
+    ev.keyCode === 18 || // Alt
+    ev.keyCode === 91 || // Meta (Left)
+    ev.keyCode === 92 || // Meta (Right)
+    ev.keyCode === 93 || // Meta (Menu)
+    ev.keyCode === 224 || // Meta (Firefox)
+    ev.key === 'Meta';
 }
diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts
index 66637e37..ba2ce6dd 100644
--- a/src/browser/Terminal.test.ts
+++ b/src/browser/Terminal.test.ts
@@ -401,6 +401,42 @@ describe('Terminal', () => {
       });
     });
 
+    describe('keyDown', () => {
+      it('should not scroll down on modifier-only input in win32 input mode', async () => {
+        term.options.vtExtensions = { win32InputMode: true };
+        term.coreService.decPrivateModes.win32InputMode = true;
+        (term as any).textarea = { value: '' };
+
+        await term.writeP('test\r\n'.repeat(term.rows * 3));
+        const startYDisp = term.buffer.ydisp;
+        term.scrollLines(-1);
+        const scrolledYDisp = term.buffer.ydisp;
+        assert.equal(scrolledYDisp, startYDisp - 1);
+
+        const evKeyDown = {
+          type: 'keydown',
+          key: 'Control',
+          keyCode: 17,
+          ctrlKey: true,
+          preventDefault: () => { },
+          stopPropagation: () => { }
+        } as KeyboardEvent;
+
+        const evKeyUp = {
+          type: 'keyup',
+          key: 'Control',
+          keyCode: 17,
+          preventDefault: () => { },
+          stopPropagation: () => { }
+        } as KeyboardEvent;
+
+        term.keyDown(evKeyDown);
+        assert.equal(term.buffer.ydisp, scrolledYDisp);
+        (term as any)._keyUp(evKeyUp);
+        assert.equal(term.buffer.ydisp, scrolledYDisp);
+      });
+    });
+
     describe('scroll() function', () => {
       describe('when scrollback > 0', () => {
         it('should create a new line and scroll', () => {

From bab7db13dbd1cc559a10632a5108db1a828d7b98 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 22:18:32 -0800
Subject: [PATCH 40/49] Remove cancelEvent/cancel function

---
 .../client/components/window/optionsWindow.ts |  1 -
 src/browser/CoreBrowserTerminal.ts            | 39 ++++++++-----------
 src/browser/TestUtils.test.ts                 |  3 --
 src/browser/Types.ts                          |  2 -
 src/common/Types.ts                           |  1 -
 src/common/services/OptionsService.ts         |  1 -
 src/common/services/Services.ts               |  1 -
 7 files changed, 17 insertions(+), 31 deletions(-)

diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts
index 1071cfd5..b97204d2 100644
--- a/demo/client/components/window/optionsWindow.ts
+++ b/demo/client/components/window/optionsWindow.ts
@@ -109,7 +109,6 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
 
   public initOptions(addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
     const blacklistedOptions = [
-      'cancelEvents',
       'convertEol',
       'termName',
       'cols', 'rows',
diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts
index 51d40224..ffc966f1 100644
--- a/src/browser/CoreBrowserTerminal.ts
+++ b/src/browser/CoreBrowserTerminal.ts
@@ -763,11 +763,12 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
             this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
           }
         }
-        return this.cancel(ev);
       },
       wheel: (ev: WheelEvent) => {
         sendEvent(ev);
-        return this.cancel(ev, true);
+        ev.preventDefault();
+        ev.stopPropagation();
+        return false;
       },
       mousedrag: (ev: MouseEvent) => {
         // deal only with move while a button is held
@@ -867,8 +868,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
       if (requestedEvents.mousedrag) {
         this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
       }
-
-      return this.cancel(ev);
     }));
 
     this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
@@ -899,13 +898,17 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
           self._coreBrowserService?.dpr
         );
         if (lines === 0) {
-          return this.cancel(ev, true);
+          ev.preventDefault();
+          ev.stopPropagation();
+          return false;
         }
 
         // Construct and send sequences
         const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
         this.coreService.triggerDataEvent(sequence, true);
-        return this.cancel(ev, true);
+        ev.preventDefault();
+        ev.stopPropagation();
+        return false;
       }
     }, { passive: false }));
   }
@@ -1115,7 +1118,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
       const scrollCount = this.rows - 1;
       this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
-      return this.cancel(event, true);
+      event.preventDefault();
+      event.stopPropagation();
+      return false;
     }
 
     if (result.type === KeyboardResultType.SELECT_ALL) {
@@ -1128,7 +1133,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
 
     if (result.cancel) {
       // The event is canceled at the end already, is this necessary?
-      this.cancel(event, true);
+      event.preventDefault();
+      event.stopPropagation();
     }
 
     if (!result.key) {
@@ -1165,7 +1171,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     // is also depressed) so that the cursor textarea can be updated, which triggers the screen
     // reader to read it.
     if (!this.optionsService.rawOptions.screenReaderMode || event.altKey || event.ctrlKey) {
-      return this.cancel(event, true);
+      event.preventDefault();
+      event.stopPropagation();
+      return false;
     }
 
     this._keyDownHandled = true;
@@ -1225,8 +1233,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
       return false;
     }
 
-    this.cancel(ev);
-
     if (ev.charCode) {
       key = ev.charCode;
     } else if (ev.which === null || ev.which === undefined) {
@@ -1279,8 +1285,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
 
       const text = ev.data;
       this.coreService.triggerDataEvent(text, true);
-
-      this.cancel(ev);
       return true;
     }
 
@@ -1392,15 +1396,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
     }
   }
 
-  // TODO: Remove cancel function and cancelEvents option
-  public cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | undefined {
-    if (!this.options.cancelEvents && !force) {
-      return;
-    }
-    ev.preventDefault();
-    ev.stopPropagation();
-    return false;
-  }
 }
 
 /**
diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts
index fe71d5b0..c46ee0fb 100644
--- a/src/browser/TestUtils.test.ts
+++ b/src/browser/TestUtils.test.ts
@@ -198,9 +198,6 @@ export class MockTerminal implements ITerminal {
   public scrollToRow(absoluteRow: number): number {
     throw new Error('Method not implemented.');
   }
-  public cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): void {
-    throw new Error('Method not implemented.');
-  }
   public log(text: string): void {
     throw new Error('Method not implemented.');
   }
diff --git a/src/browser/Types.ts b/src/browser/Types.ts
index f54e488c..fa08de2e 100644
--- a/src/browser/Types.ts
+++ b/src/browser/Types.ts
@@ -29,8 +29,6 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
   onA11yChar: IEvent;
   onA11yTab: IEvent;
   onWillOpen: IEvent;
-
-  cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | void;
 }
 
 export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
diff --git a/src/common/Types.ts b/src/common/Types.ts
index 5d7c1b18..a46f47b2 100644
--- a/src/common/Types.ts
+++ b/src/common/Types.ts
@@ -32,7 +32,6 @@ export interface IDisposable {
 // TODO: The options that are not in the public API should be reviewed
 export interface ITerminalOptions extends IPublicTerminalOptions {
   [key: string]: any;
-  cancelEvents?: boolean;
   convertEol?: boolean;
   termName?: string;
 }
diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts
index 2eab930f..7ddc1c06 100644
--- a/src/common/services/OptionsService.ts
+++ b/src/common/services/OptionsService.ts
@@ -53,7 +53,6 @@ export const DEFAULT_OPTIONS: Readonly> = {
   altClickMovesCursor: true,
   convertEol: false,
   termName: 'xterm',
-  cancelEvents: false,
   overviewRuler: {},
   quirks: {},
   vtExtensions: {}
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 341c0bc2..9ef0fe9a 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -270,7 +270,6 @@ export interface ITerminalOptions {
   vtExtensions?: IVtExtensions;
 
   [key: string]: any;
-  cancelEvents: boolean;
   termName: string;
 }
 

From 7cba3c9fa134404906a0c152dc732707d528982e Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 22:26:06 -0800
Subject: [PATCH 41/49] Correct test copilot instructions

---
 .github/copilot-instructions.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 461f8fdb..c9954ed7 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -22,10 +22,10 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
 **Testing**:
 - Unit tests: `npm run test-unit` (Mocha)
 - Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
-- Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js`
+- Per-addon unit tests: `npm run test-unit -- addons/addon-image/out-esbuild/*.test.js`
 - Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
 - Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
-- Integration tests by addon: `npm run test-integration --suite=addon-search`. Suites always follow the format `addon-`
+- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-`
 - Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
 
 ## Addon Development Pattern

From a27b8feb8f778b57a8aaed57bdfa0bbab1553e8a Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sat, 31 Jan 2026 22:56:48 -0800
Subject: [PATCH 42/49] Fix demo button outline

---
 demo/index.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/demo/index.css b/demo/index.css
index b3123eb2..6880bde4 100644
--- a/demo/index.css
+++ b/demo/index.css
@@ -33,6 +33,7 @@ body {
     padding: 4px 10px;
     cursor: pointer;
     font-size: 12px;
+    outline-offset: -1px;
 }
 .banner-tabs button:hover {
     background: rgba(255,255,255,0.1);

From a5e4f90139d5e575383b353005f5cfeb6b8cac62 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sun, 1 Feb 2026 05:58:56 -0800
Subject: [PATCH 43/49] Remove unreachable code in SGR handling 100

The implementation seems to be wrong. This looks like a remnant left over from
term.js before bright SGR was implemented.
---
 src/common/InputHandler.ts | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index 11381ce0..f5c2a9c6 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -2713,12 +2713,6 @@ export class InputHandler extends Disposable implements IInputHandler {
         attr.extended = attr.extended.clone();
         attr.extended.underlineColor = -1;
         attr.updateExtended();
-      } else if (p === 100) { // FIXME: dead branch, p=100 already handled above!
-        // reset fg/bg
-        attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
-        attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
-        attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
-        attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
       } else {
         this._logService.debug('Unknown SGR attribute: %d.', p);
       }

From 6525a7e68e45c8019575cb190bc255fb449a4afa Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sun, 1 Feb 2026 07:15:25 -0800
Subject: [PATCH 44/49] Fix copying array data in IIPHandler

Fixes #5660
---
 addons/addon-image/src/IIPHandler.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/addons/addon-image/src/IIPHandler.ts b/addons/addon-image/src/IIPHandler.ts
index bebfee11..4922857a 100644
--- a/addons/addon-image/src/IIPHandler.ts
+++ b/addons/addon-image/src/IIPHandler.ts
@@ -105,7 +105,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
       return true;
     }
 
-    const blob = new Blob([new Uint8Array(this._dec.data8)], { type: this._metrics.mime });
+    // HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
+    const blob = new Blob([this._dec.data8 as Uint8Array], { type: this._metrics.mime });
     this._dec.release();
 
     if (!window.createImageBitmap) {

From 5316cb081219356a773835c6c3cb1790327a8f36 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sun, 1 Feb 2026 07:48:40 -0800
Subject: [PATCH 45/49] Remove some easy any usages

---
 addons/addon-image/src/IIPHandler.ts             |  4 ----
 addons/addon-image/src/IIPHeaderParser.ts        |  4 ++--
 addons/addon-image/src/ImageStorage.ts           |  6 ++++--
 addons/addon-ligatures/src/font.ts               | 16 ++++++++++------
 addons/addon-ligatures/src/index.ts              |  2 +-
 addons/addon-search/src/SearchAddon.ts           |  4 ++--
 .../src/third-party/UnicodeProperties.ts         |  2 +-
 src/common/parser/EscapeSequenceParser.ts        |  3 +--
 8 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/addons/addon-image/src/IIPHandler.ts b/addons/addon-image/src/IIPHandler.ts
index bebfee11..af6a681a 100644
--- a/addons/addon-image/src/IIPHandler.ts
+++ b/addons/addon-image/src/IIPHandler.ts
@@ -9,10 +9,6 @@ import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
 import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
 import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
 
-
-// eslint-disable-next-line
-declare const Buffer: any;
-
 // limit hold memory in base64 decoder
 const KEEP_DATA = 4194304;
 
diff --git a/addons/addon-image/src/IIPHeaderParser.ts b/addons/addon-image/src/IIPHeaderParser.ts
index dd872fed..53c1edf3 100644
--- a/addons/addon-image/src/IIPHeaderParser.ts
+++ b/addons/addon-image/src/IIPHeaderParser.ts
@@ -81,7 +81,7 @@ function toName(data: Uint32Array): string {
   return new TextDecoder().decode(b);
 }
 
-const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
+const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
   inline: toInt,
   size: toInt,
   name: toName,
@@ -100,7 +100,7 @@ export class HeaderParser {
   private _buffer = new Uint32Array(MAX_FIELDCHARS);
   private _position = 0;
   private _key = '';
-  public fields: {[key: string]: any} = {};
+  public fields: {[key: string]: number | string | Uint32Array | null} = {};
 
   public reset(): void {
     this._buffer.fill(0);
diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts
index 059849ea..aea9b5e5 100644
--- a/addons/addon-image/src/ImageStorage.ts
+++ b/addons/addon-image/src/ImageStorage.ts
@@ -132,8 +132,10 @@ export class ImageStorage implements IDisposable {
   ) {
     try {
       this.setLimit(this._opts.storageLimit);
-    } catch (e: any) {
-      console.error(e.message);
+    } catch (e: unknown) {
+      if (e instanceof Error) {
+        console.error(e.message);
+      }
       console.warn(`storageLimit is set to ${this.getLimit()} MB`);
     }
     this._viewportMetrics = {
diff --git a/addons/addon-ligatures/src/font.ts b/addons/addon-ligatures/src/font.ts
index 1316932a..60ab3d74 100644
--- a/addons/addon-ligatures/src/font.ts
+++ b/addons/addon-ligatures/src/font.ts
@@ -42,11 +42,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
         if (status && status.state !== 'granted') {
           throw new Error('Permission to access local fonts not granted.');
         }
-      } catch (err: any) {
+      } catch (err: unknown) {
         // A `TypeError` indicates the 'local-fonts'
         // permission is not yet implemented, so
         // only `throw` if this is _not_ the problem.
-        if (err.name !== 'TypeError') {
+        if (err instanceof Error && err.name !== 'TypeError') {
           throw err;
         }
       }
@@ -60,8 +60,10 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
           fonts[metadata.family].push(metadata);
         }
         fontsPromise = Promise.resolve(fonts);
-      } catch (err: any) {
-        console.error(err.name, err.message);
+      } catch (err: unknown) {
+        if (err instanceof Error) {
+          console.error(err.name, err.message);
+        }
       }
     }
     // Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
@@ -76,8 +78,10 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
           fonts[metadata.family].push(metadata);
         }
         fontsPromise = Promise.resolve(fonts);
-      } catch (err: any) {
-        console.error(err.name, err.message);
+      } catch (err: unknown) {
+        if (err instanceof Error) {
+          console.error(err.name, err.message);
+        }
       }
     }
     fontsPromise ??= Promise.resolve({});
diff --git a/addons/addon-ligatures/src/index.ts b/addons/addon-ligatures/src/index.ts
index 0c67f510..fb8e85e7 100644
--- a/addons/addon-ligatures/src/index.ts
+++ b/addons/addon-ligatures/src/index.ts
@@ -30,7 +30,7 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
   let currentFontName: string | undefined = undefined;
   let font: Font | undefined = undefined;
   let loadingState: LoadingState = LoadingState.UNLOADED;
-  let loadError: any | undefined = undefined;
+  let loadError: unknown = undefined;
 
   return term.registerCharacterJoiner((text: string): [number, number][] => {
     // If the font hasn't been loaded yet, load it and return an empty result
diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts
index ffbe9401..8e647a9e 100644
--- a/addons/addon-search/src/SearchAddon.ts
+++ b/addons/addon-search/src/SearchAddon.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
-import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
+import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
 import { Emitter, Event } from 'vs/base/common/event';
 import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
 import { disposableTimeout } from 'vs/base/common/async';
@@ -222,7 +222,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
    * @param result The result to select.
    * @returns Whether a result was selected.
    */
-  private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
+  private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
     if (!this._terminal || !this._decorationManager) {
       return false;
     }
diff --git a/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts b/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts
index 0ee147f8..e0af5ce8 100644
--- a/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts
+++ b/addons/addon-unicode-graphemes/src/third-party/UnicodeProperties.ts
@@ -1,7 +1,7 @@
 import UnicodeTrie from './unicode-trie';
 const trieRaw = "AAARAAAAAABwxwAAAb4LQfTtmw+sVmUdx58LL/ffe/kjzNBV80gW1F3yR+6CvbJiypoZa0paWmAWSluErSBbFtYkkuZykq6QamGJ4WRqo2kFGy6dYWtEq6G1MFAJbRbOVTQr+x7f5+x97q/n/3me87wXzm/3s+f/7/d7/p7znnvOlvGMbQM7wIPgEbAPHABPgcPgefAS+BfYwuv/F/Q2OulBxKcK6TMRPxu8FcwFbwcjYCFYDC4Cl4ArwNXgGvBJsA58UdBDwy+jbBO4La8DtoEd4H7wkNBuN+KPgn3gADgIngaHwFHwF/AyeAWMm4C+TGi3LdiJ/EnIex04A2RgFpgD5oKFYDG4CLwHXAo+IKSvAqt4/evA9bz9jWA6+Cq3dyvCP8HWNwX93wF38/ROcD94SCjP2+1B+BiPP4HwgOD/7xD/I08fRniMx48jPAFeBeuF+n29jE0G08FZvaPHYWZvh9mcEfAOjlhXx/qGfd2QvLO3zccmtMnzliC9lPt+GenD1nyMiK/LNf1cycs+gfAzPJ6vtxe4jhuQtx5sBLeA28G3eb3v8/Beif4HkPewxu5G6N/rMP4qfgEdvwZPgj+AZ8Cx3nYfxiE8Dk6AV0FfH/YEOB28AbwJDIPzQAtcAC4Gl/Z19F+J+NVCehWPr0b46b7RvixvdPg8yr7U10l/BfFN4La8DdgGdoAHwU/AI2AfOACeAofB8+AlcAKwfvyBKeCM/o7NrF9PXmdWv9/Ynot2I7ztIg8dF5I2a8i63CjZU+9Fm2Wcy4U4ZQVYyeOrwVoev57UuxHcJKRvFuJXgnU8/nUebtbYrKmpCUOx31P7UVNTU1NTU1NTU1OGLTz8Xr/77+W7+9vP0or0MxPMbXaizY8FW3sQ3wseB/t5/kGEh8DR/vbzwL8i/Af4Dy8fP8BYE0weaKenI/wV/DhrQG97JspngzlgLpgHzgPzwUhdVpfVZXVZXRa87HxwAVgQ4Pn5WEd85l5TUzOasvezFw/E3b/LoP9D4CpwrcTWWsGXNQOj748/G9k3G56d1KYxmbELwQbwKFiJvBM8nDWlHa5E+AOwCzwLzjkNeeB28NvTeB1OYyr0gQ1g99R23nGE50xj7MPgc+A+8K5Bxj4FHgB/G2z/T9XEzCZjd/S0WYX4Pc3/r/Nn5I0f6qQXIP5x8ENwBMyYyNhHJ3b0pOCuLrBvM941NTU1JyNHEp+BrC8dMyalt1/m3uWfhmeULzRGp9d3wf0WZSN8+prCr60Wz09tuNmx35sl9Y825HXvRN39KNveaL8flb9f913kbec67kHeTsR3gYcH2uV7ED4m2HhCYi/X9ZuBzvuXv0f8iKIfx5B/XCg7gTgbVPdvAsomCuWnD45eK28UyvL3Jt+s0fU2TVnOXJQvJHUWIb0ELAWXgCt4+UcMumSsEtpch/g6ouMGpG/ieZsc9N/q4YsLd3D9WyPbsWEbfNgO7hN82TWY/n8xKbmsC3xQsYKf+7sjrx2TH+u4H3vhx+OO6+X9hmtXN7C/4r15EPaeBs9J7L7YBeeED/k7wn8fbIf/Rji+yVizmd4vW6bB19cb/PU9w7MxMA60bzPHgM8+zG623+OnzOf55yNc3Gw/k303wveBy3nZcoTXgNVgLfiCRNcG5N3SbIebwZ08fhe4l8d/BH7K4yI/4+HPwS/BAfBks+PzIaHuc3x+ivSL4GUyZ68I6fwZYRNMG2qnz+Th2QjfMtTx/1zE5w61nyN+Q7C3aKgdin1dgrylYBn4INdhGn/Z2FfFiqH01/SUXMvnPD+jC+j85N/RqRhR/DYaS6T+P09K1mD+vzW+5zVqqeVUl0wTz2lK8odJHRGXfBufdGLSoSo3+ZFJ6sl0qvJVNmhI4z4i06mrZ6uT1le1z5h5HE3tMiHPtQ5javu+ItMXUr/MXpmwmyRL3D6U7UwIMyYfczGu0qdqb2pbhcw4xQkhWQBMerrZ/liXrGTbsQwTwrEu4zSczKLrd7fCSKiKn+zSo8BWXMe8myXWOivrUxWi60OPoQ7VIasbQ0S/Ukk3rZVullNhHEL1rYoxUF0PTfm6elWJzq54ZsU4z11ohOy0oxT2izFqCNj4TesXcWZo6+Jfqr1O+1O1beqDagypj2J9F1u2daucj3Eknmq/6PaHrK7Mb1o35DiW1a/a76LuhlDXZX25SOz11S33ErKxDb2/fc/bFKI6axskn+4/W90u9mOtbRf7smsoTdvOfwoRz0t6DaP9k81v6P7Re5aUQudTd303rX+bZzBl97/KR7E+Xbux9lLI+aNr1PfaYLpPDiW2/vrYTX1drMIeXbMye6HXlw8292Jl7ZXxLxRlxXbcaH9drjFlxfa3Qozx8NWRi834lPVZbD+SmN7EJPzc9TVCSVXXDps9L+513b2J7fMu176V2YOhx1A3JrJ8KrLxUumpcu5j/lYT+2tzLRVDZmhjO442a1Clu0ox9VPVXzE/lcS4V0k1D6LI1pJsz8fct9SGbO5l/rmKzTlvsxdj3IvRtC2uv0t1fotltvd2VaCy5Sp5m0EhnZG4CCNxXZrWp/VUIrOjapfnNw11ZNI0V/GWzKNuxtzGKKTEtJeR0NVmpojbtBuW5On0u0is9ZMxvU8ZM+8vEyadtu10oqtP9Q4rcJEm85+Two/QkpGwjI6YkgkhtUfzZOW6fFVexuRri+qj9TJJHZkdmW5abiu0rs6uj2TMfmx06bISUj9tZ9Lja8dVQtox6WpxTJKfW3M4MSTmvU4sWy1CU6BF4jIfdNeDjHWuO1lCWIm2Jr2ixNZvklD2fP0Q6+vsmO4hqN1hJvfDtV5G8mTlsvau4qPP1a64L1skT6QYEzEtq0PzGZOfCbSdSmcKTP7Qs86Ej/1hEpelaV6IMdT5ayu2+nT9tmnnO746XbLxE8t0qOrYtJWhmk9bvaLfsrotRVw1PnR+bcafSUKZ6Mps7smobybJLH2R6WqRkJa1DHV0UmbfUcksiSF0HExSpp+uY0zbTklMaCm7blzEtg8h1rNMXNaYi05ZXsbC75sQ/4+aUxFV2jL50Q3jE0rK2rVtN09By8OHoo1vH2LPSdE323mr2sdu0pUZiDkWLRKWnfeQY6taKzHF9n/GPv8jd/0/egiRvYMR24fU79iY3s9Qva9RlYR8n8HHtq9fMcT1HRWfdZXiHd9YInt/iI4PTaf+BimXKvdXYU+3hlRpHzs2dVK/cxhDn+xs0I2jzxjL5kpXz1VU72aLtkK/97sALKyQqu25SshvG6h08/cLrlKswRklKXvvXfa+pZt+y8nah5YUv2Oo/ap/X2URdRfico9K69hcp6r6XaCz5Wo/hs/iNTGF6N6tV92/9ZS0Wba9SlT3pKF/e6W674+x9ly+VRL73cPU8ygb31D3eSqfVd+iqET0y3YMYojoO11XqrTt2nPxmeq1HYeqxkmUMt8DiesjpoTSr+qDrD+qPZDiOZxMdH0pRPX8MFUfQtv0Xbs+a1a1NnRryNZ/2+tsaPG5ZoX0RXZei88yZGdo4UMPj/cwv/kMJboxLISuQbE+1VW12Mx7FWOrW3M9Hv7Y+uxyraPSo8B2TGPuLdOeZha+hBKf8Sjsm/oR+7pmsx/oeOraFWdXleeV6oyl41zm+mgSuq9C6ox1TsU8D+m4dwMmf8v2nz7Tm+fYfj7HV1K/x1HWjquvY+2dllxM64ue87Su772zzbXIVC+WxLZTRR9MdkMTypZNH1z6G0tUvoccwxA+hfLNdV+a7MaQqscztMi+7QnxDZXvd1dldWQOyMbApb1Jd2h91Ffx+y9Xfb7tClokboOvrRhrbVpFFO8z+65t2/u4su9MUx028znH01/TGVDmHAj13W1o+1USw+eUfYtpO+b82rRNsb6oPpV+1fdBqddB6n3WDXvdJDZrJ0QfQp6bsc/kqq4BIddHWXGdN1pmWveh58F1zYUW1zmOITHXWOg1XrZvZSWUf77tq1ofqear6muaT1lIQp3bofabSafJVlnfYo9B6LGr8uzz2Xchvzfw+T9PlgiV/A8=";
 
-declare const Buffer: any;
+declare const Buffer: { from(s: string, encoding: string): Uint8Array } | undefined;
 function _dec(s: string): Uint8Array {
     if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
     const bs = atob(s);
diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts
index bb99efe1..febb4878 100644
--- a/src/common/parser/EscapeSequenceParser.ts
+++ b/src/common/parser/EscapeSequenceParser.ts
@@ -91,14 +91,13 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
   EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
 
   const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);
-  let state: any;
 
   // set default transition
   table.setDefault(ParserAction.ERROR, ParserState.GROUND);
   // printables
   table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
   // global anywhere rules
-  for (state in states) {
+  for (const state of states) {
     table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);
     table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);
     table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);

From 297ac838a376c9a82654a6dd23f82de7c4c23eda Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sun, 1 Feb 2026 07:58:36 -0800
Subject: [PATCH 46/49] Fix build

---
 addons/addon-image/src/IIPHeaderParser.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/addons/addon-image/src/IIPHeaderParser.ts b/addons/addon-image/src/IIPHeaderParser.ts
index 53c1edf3..af4e9a68 100644
--- a/addons/addon-image/src/IIPHeaderParser.ts
+++ b/addons/addon-image/src/IIPHeaderParser.ts
@@ -8,6 +8,7 @@ declare const Buffer: any;
 
 
 export interface IHeaderFields {
+  [key: string]: number | string | Uint32Array | null | undefined;
   // base-64 encoded filename. Defaults to "Unnamed file".
   name: string;
   // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -100,7 +101,7 @@ export class HeaderParser {
   private _buffer = new Uint32Array(MAX_FIELDCHARS);
   private _position = 0;
   private _key = '';
-  public fields: {[key: string]: number | string | Uint32Array | null} = {};
+  public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
 
   public reset(): void {
     this._buffer.fill(0);

From 53efc951700dd2cdccc7dc9839632aa484da5b71 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Sun, 1 Feb 2026 08:26:20 -0800
Subject: [PATCH 47/49] Move remaining addon test buttons into their own
 windows

---
 demo/client/client.ts                         |  33 ++-
 .../components/window/addonImageWindow.ts     | 104 ++++++++
 .../components/window/addonLigaturesWindow.ts |  44 ++++
 .../components/window/addonProgressWindow.ts  | 135 ++++++++++
 .../components/window/addonWebLinksWindow.ts  |  52 ++++
 demo/client/components/window/testWindow.ts   | 238 ------------------
 6 files changed, 362 insertions(+), 244 deletions(-)
 create mode 100644 demo/client/components/window/addonLigaturesWindow.ts
 create mode 100644 demo/client/components/window/addonProgressWindow.ts
 create mode 100644 demo/client/components/window/addonWebLinksWindow.ts

diff --git a/demo/client/client.ts b/demo/client/client.ts
index 45bdd1d4..43fa1413 100644
--- a/demo/client/client.ts
+++ b/demo/client/client.ts
@@ -16,9 +16,12 @@ if ('WebAssembly' in window) {
 import { Terminal, ITerminalOptions, type ITheme } from '@xterm/xterm';
 import { AttachAddon } from '@xterm/addon-attach';
 import { AddonImageWindow } from './components/window/addonImageWindow';
+import { AddonLigaturesWindow } from './components/window/addonLigaturesWindow';
+import { AddonProgressWindow } from './components/window/addonProgressWindow';
 import { AddonSearchWindow } from './components/window/addonSearchWindow';
 import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
 import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
+import { AddonWebLinksWindow } from './components/window/addonWebLinksWindow';
 import { AddonsWindow } from './components/window/addonsWindow';
 import { CellInspectorWindow } from './components/window/cellInspectorWindow';
 import { ControlBar } from './components/controlBar';
@@ -213,11 +216,14 @@ if (document.location.pathname === '/test') {
   controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
   controlBar.registerWindow(new VtWindow(typedTerm, addons));
   addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
-  addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonLigaturesWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonProgressWindow(typedTerm, addons), { afterId: 'addon-ligatures', hidden: true, italics: true });
+  addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addon-progress', hidden: true, italics: true });
   controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
-  controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
-  controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
-  addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
+  controlBar.registerWindow(new AddonWebLinksWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
+  addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-links', hidden: true, italics: true });
   controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
   actionElements = {
     findNext: addonSearchWindow.findNextInput,
@@ -229,11 +235,14 @@ if (document.location.pathname === '/test') {
   // TODO: Most of below should be encapsulated within windows
   paddingElement = styleWindow.paddingElement;
 
-  controlBar.setTabVisible('addon-webgl', true);
+  controlBar.setTabVisible('addon-image', !!addons.image.instance);
+  controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
+  controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
   controlBar.setTabVisible('addon-search', true);
   controlBar.setTabVisible('addon-serialize', true);
-  controlBar.setTabVisible('addon-image', true);
   controlBar.setTabVisible('addon-web-fonts', true);
+  controlBar.setTabVisible('addon-web-links', !!addons.webLinks.instance);
+  controlBar.setTabVisible('addon-webgl', true);
   addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
   addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
   addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
@@ -500,6 +509,12 @@ function initAddons(term: Terminal): void {
             addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
           } else if (name === 'serialize') {
             controlBar.setTabVisible('addon-serialize', true);
+          } else if (name === 'ligatures') {
+            controlBar.setTabVisible('addon-ligatures', true);
+          } else if (name === 'progress') {
+            controlBar.setTabVisible('addon-progress', true);
+          } else if (name === 'webLinks') {
+            controlBar.setTabVisible('addon-web-links', true);
           }
         }
         catch {
@@ -516,6 +531,12 @@ function initAddons(term: Terminal): void {
           controlBar.setTabVisible('addon-search', false);
         } else if (name === 'serialize') {
           controlBar.setTabVisible('addon-serialize', false);
+        } else if (name === 'ligatures') {
+          controlBar.setTabVisible('addon-ligatures', false);
+        } else if (name === 'progress') {
+          controlBar.setTabVisible('addon-progress', false);
+        } else if (name === 'webLinks') {
+          controlBar.setTabVisible('addon-web-links', false);
         }
         addon.instance!.dispose();
         addon.instance = undefined;
diff --git a/demo/client/components/window/addonImageWindow.ts b/demo/client/components/window/addonImageWindow.ts
index 87ebaff7..ce5ab9de 100644
--- a/demo/client/components/window/addonImageWindow.ts
+++ b/demo/client/components/window/addonImageWindow.ts
@@ -5,6 +5,7 @@
 
 import { BaseWindow } from './baseWindow';
 import type { IControlWindow } from '../controlBar';
+import type { IImageAddonOptions } from '@xterm/addon-image';
 
 export class AddonImageWindow extends BaseWindow implements IControlWindow {
   public readonly id = 'addon-image';
@@ -46,6 +47,20 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
     this._imageOptionsTextarea.rows = 12;
     optionsLabel.appendChild(this._imageOptionsTextarea);
     container.appendChild(optionsLabel);
+
+    container.appendChild(document.createElement('br'));
+    container.appendChild(document.createElement('br'));
+
+    const dl = document.createElement('dl');
+    const dt = document.createElement('dt');
+    dt.textContent = 'Image Test';
+    dl.appendChild(dt);
+    this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)');
+    this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)');
+    this._addDdWithButton(dl, 'image-demo3', 'palette (iip)');
+    container.appendChild(dl);
+
+    this._initImageAddonExposed();
   }
 
   public get imageStorageLimitInput(): HTMLInputElement {
@@ -59,4 +74,93 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
   public get imageOptionsTextarea(): HTMLTextAreaElement {
     return this._imageOptionsTextarea;
   }
+
+  private _addDdWithButton(dl: HTMLElement, id: string, label: string): void {
+    const dd = document.createElement('dd');
+    const button = document.createElement('button');
+    button.id = id;
+    button.textContent = label;
+    dd.appendChild(button);
+    dl.appendChild(dd);
+  }
+
+  private _initImageAddonExposed(): void {
+    const imageAddon = this._addons.image.instance!;
+    const defaultOptions: IImageAddonOptions = (imageAddon as any)._defaultOpts;
+    const limitStorageElement = document.querySelector('#image-storagelimit')!;
+    limitStorageElement.valueAsNumber = imageAddon.storageLimit;
+    this._addDomListener(limitStorageElement, 'change', () => {
+      try {
+        imageAddon.storageLimit = limitStorageElement.valueAsNumber;
+        limitStorageElement.valueAsNumber = imageAddon.storageLimit;
+        console.log('changed storageLimit to', imageAddon.storageLimit);
+      } catch (e) {
+        limitStorageElement.valueAsNumber = imageAddon.storageLimit;
+        console.log('storageLimit at', imageAddon.storageLimit);
+        throw e;
+      }
+    });
+    const showPlaceholderElement = document.querySelector('#image-showplaceholder')!;
+    showPlaceholderElement.checked = imageAddon.showPlaceholder;
+    this._addDomListener(showPlaceholderElement, 'change', () => {
+      imageAddon.showPlaceholder = showPlaceholderElement.checked;
+    });
+    const ctorOptionsElement = document.querySelector('#image-options')!;
+    ctorOptionsElement.value = JSON.stringify(defaultOptions, null, 2);
+
+    const sixelDemo = (url: string) => () => fetch(url)
+      .then(resp => resp.arrayBuffer())
+      .then(buffer => {
+        this._terminal.write('\r\n');
+        this._terminal.write(new Uint8Array(buffer));
+      });
+
+    const iipDemo = (url: string) => () => fetch(url)
+      .then(resp => resp.arrayBuffer())
+      .then(buffer => {
+        const data = new Uint8Array(buffer);
+        let sdata = '';
+        for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
+        this._terminal.write('\r\n');
+        this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
+      });
+
+    document.getElementById('image-demo1')!.addEventListener('click',
+      sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six'));
+    document.getElementById('image-demo2')!.addEventListener('click',
+      sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
+    document.getElementById('image-demo3')!.addEventListener('click',
+      iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
+
+    // demo for image retrieval API
+    this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
+      if (!ev.ctrlKey || !imageAddon) return;
+
+      // TODO...
+      // if (ev.altKey) {
+      //   const sel = term.getSelectionPosition();
+      //   if (sel) {
+      //     addons.image.instance
+      //       .extractCanvasAtBufferRange(term.getSelectionPosition())
+      //       ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
+      //     return;
+      //   }
+      // }
+
+      const pos = (this._terminal as any)._core._mouseService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
+      const x = pos[0] - 1;
+      const y = pos[1] - 1;
+      const canvas = ev.shiftKey
+        // ctrl+shift+click: get single tile
+        ? imageAddon.extractTileAtBufferCell(x, this._terminal.buffer.active.viewportY + y)
+        // ctrl+click: get original image
+        : imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
+      canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
+    });
+  }
+
+  private _addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
+    element.addEventListener(type, handler);
+    (this._terminal as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
+  }
 }
diff --git a/demo/client/components/window/addonLigaturesWindow.ts b/demo/client/components/window/addonLigaturesWindow.ts
new file mode 100644
index 00000000..273ec04e
--- /dev/null
+++ b/demo/client/components/window/addonLigaturesWindow.ts
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { BaseWindow } from './baseWindow';
+import type { IControlWindow } from '../controlBar';
+
+export class AddonLigaturesWindow extends BaseWindow implements IControlWindow {
+  public readonly id = 'addon-ligatures';
+  public readonly label = 'ligatures';
+
+  public build(container: HTMLElement): void {
+    const dl = document.createElement('dl');
+    const dt = document.createElement('dt');
+    dt.textContent = 'Ligatures Addon';
+    dl.appendChild(dt);
+
+    const dd = document.createElement('dd');
+    const button = document.createElement('button');
+    button.id = 'ligatures-test';
+    button.textContent = 'Common ligatures';
+    button.title = 'Write common ligatures sequences';
+    button.addEventListener('click', () => this._ligaturesTest());
+    dd.appendChild(button);
+    dl.appendChild(dd);
+
+    container.appendChild(dl);
+  }
+
+  private _ligaturesTest(): void {
+    this._terminal.write([
+      '',
+      '-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
+      '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
+      '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
+      '<~~  /> ~~> == != /= ~= <> === !== !=== =/= =!=',
+      '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
+      '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|  ---> ->- >- >>-',
-    '=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
-    '<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
-    '<~~  /> ~~> == != /= ~= <> === !== !=== =/= =!=',
-    '<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
-    '(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -|