From c624a47780550bdf2fe8e616179e62596e634bf6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:59:50 -0800 Subject: [PATCH] Maintain different buffer flags, stack limit, tests --- src/common/InputHandler.test.ts | 143 +++++++++++++++++++---------- src/common/InputHandler.ts | 17 ++++ src/common/TestUtils.test.ts | 2 + src/common/Types.ts | 6 +- src/common/services/CoreService.ts | 2 + 5 files changed, 122 insertions(+), 48 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index ee12e9a0..59966fd7 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -2413,62 +2413,111 @@ describe('InputHandler', () => { } }); }); -}); + describe('InputHandler - kitty keyboard', () => { + let bufferService: IBufferService; + let coreService: ICoreService; + let optionsService: MockOptionsService; + let inputHandler: TestInputHandler; -describe('InputHandler - async handlers', () => { - let bufferService: IBufferService; - let coreService: ICoreService; - let optionsService: MockOptionsService; - let inputHandler: TestInputHandler; + beforeEach(() => { + optionsService = new MockOptionsService({ vtExtensions: { kittyKeyboard: true } }); + bufferService = new BufferService(optionsService); + bufferService.resize(80, 30); + coreService = new CoreService(bufferService, new MockLogService(), optionsService); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + }); - beforeEach(() => { - optionsService = new MockOptionsService(); - bufferService = new BufferService(optionsService); - bufferService.resize(80, 30); - coreService = new CoreService(bufferService, new MockLogService(), optionsService); - coreService.onData(data => { console.log(data); }); + describe('stack limit', () => { + it('should evict oldest entry when stack exceeds 16 entries', async () => { + for (let i = 1; i <= 20; i++) { + await inputHandler.parseP(`\x1b[>${i}u`); + } + assert.strictEqual(coreService.kittyKeyboard.mainStack.length, 16); + assert.strictEqual(coreService.kittyKeyboard.mainStack[0], 4); + }); + }); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + describe('buffer switch', () => { + it('should maintain separate flags for main and alt screens', async () => { + await inputHandler.parseP('\x1b[>5u'); + assert.strictEqual(coreService.kittyKeyboard.flags, 5); + await inputHandler.parseP('\x1b[?1049h'); + assert.strictEqual(coreService.kittyKeyboard.flags, 0); + assert.strictEqual(coreService.kittyKeyboard.mainFlags, 5); + await inputHandler.parseP('\x1b[>7u'); + assert.strictEqual(coreService.kittyKeyboard.flags, 7); + await inputHandler.parseP('\x1b[?1049l'); + assert.strictEqual(coreService.kittyKeyboard.flags, 5); + assert.strictEqual(coreService.kittyKeyboard.altFlags, 7); + }); + }); + + describe('pop reset', () => { + it('should reset flags to 0 when stack is emptied', async () => { + await inputHandler.parseP('\x1b[>5u'); + assert.strictEqual(coreService.kittyKeyboard.flags, 5); + await inputHandler.parseP('\x1b[<10u'); + assert.strictEqual(coreService.kittyKeyboard.flags, 0); + }); + }); }); - it('async CUP with CPR check', async () => { - const cup: number[][] = []; - const cpr: number[][] = []; - inputHandler.registerCsiHandler({ final: 'H' }, async params => { - cup.push(params.toArray() as number[]); - await new Promise(res => setTimeout(res, 50)); - // late call of real repositioning - return inputHandler.cursorPosition(params); + + describe('InputHandler - async handlers', () => { + let bufferService: IBufferService; + let coreService: ICoreService; + let optionsService: MockOptionsService; + let inputHandler: TestInputHandler; + + beforeEach(() => { + optionsService = new MockOptionsService(); + bufferService = new BufferService(optionsService); + bufferService.resize(80, 30); + coreService = new CoreService(bufferService, new MockLogService(), optionsService); + coreService.onData(data => { console.log(data); }); + + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); - coreService.onData(data => { - const m = data.match(/\x1b\[(.*?);(.*?)R/); - if (m) { - cpr.push([parseInt(m[1]), parseInt(m[2])]); - } + + it('async CUP with CPR check', async () => { + const cup: number[][] = []; + const cpr: number[][] = []; + inputHandler.registerCsiHandler({ final: 'H' }, async params => { + cup.push(params.toArray() as number[]); + await new Promise(res => setTimeout(res, 50)); + // late call of real repositioning + return inputHandler.cursorPosition(params); + }); + coreService.onData(data => { + const m = data.match(/\x1b\[(.*?);(.*?)R/); + if (m) { + cpr.push([parseInt(m[1]), parseInt(m[2])]); + } + }); + await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n'); + assert.deepEqual(cup, cpr); }); - await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n'); - assert.deepEqual(cup, cpr); - }); - it('async OSC between', async () => { - inputHandler.registerOscHandler(1000, async data => { - await new Promise(res => setTimeout(res, 50)); - assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); - assert.equal(data, 'some data'); - return true; + it('async OSC between', async () => { + inputHandler.registerOscHandler(1000, async data => { + await new Promise(res => setTimeout(res, 50)); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); + assert.equal(data, 'some data'); + return true; + }); + await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line'); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); }); - await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line'); - assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); - }); - it('async DCS between', async () => { - inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => { - await new Promise(res => setTimeout(res, 50)); - assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); - assert.equal(data, 'some data'); - assert.deepEqual(params.toArray(), [1, 2]); - return true; + it('async DCS between', async () => { + inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => { + await new Promise(res => setTimeout(res, 50)); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); + assert.equal(data, 'some data'); + assert.deepEqual(params.toArray(), [1, 2]); + return true; + }); + await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line'); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); }); - await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line'); - assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); }); }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 6ba8abb7..fa252113 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2009,6 +2009,12 @@ export class InputHandler extends Disposable implements IInputHandler { // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer + // Swap kitty keyboard flags: save main, restore alt + if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) { + const state = this._coreService.kittyKeyboard; + state.mainFlags = state.flags; + state.flags = state.altFlags; + } this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()); this._coreService.isCursorInitialized = true; this._onRequestRefreshRows.fire(undefined); @@ -2238,6 +2244,12 @@ export class InputHandler extends Disposable implements IInputHandler { // FALL-THROUGH case 47: // normal screen buffer case 1047: // normal screen buffer - clearing it first + // Swap kitty keyboard flags: save alt, restore main + if (this._optionsService.rawOptions.vtExtensions?.kittyKeyboard) { + const state = this._coreService.kittyKeyboard; + state.altFlags = state.flags; + state.flags = state.mainFlags; + } // Ensure the selection manager has the correct buffer this._bufferService.buffers.activateNormalBuffer(); if (params.params[i] === 1049) { @@ -3043,6 +3055,11 @@ export class InputHandler extends Disposable implements IInputHandler { const isAlt = this._bufferService.buffer === this._bufferService.buffers.alt; const stack = isAlt ? state.altStack : state.mainStack; + // Evict oldest entry if stack is full (DoS protection, limit of 16) + if (stack.length >= 16) { + stack.shift(); + } + // Push current flags onto stack and set new flags stack.push(state.flags); state.flags = flags; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 6ba72692..9bfb2d19 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -116,6 +116,8 @@ export class MockCoreService implements ICoreService { }; public kittyKeyboard = { flags: 0, + mainFlags: 0, + altFlags: 0, mainStack: [] as number[], altStack: [] as number[] }; diff --git a/src/common/Types.ts b/src/common/Types.ts index 88a466b7..9ad294f0 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -282,8 +282,12 @@ export interface IDecPrivateModes { * Maintains per-screen stacks of enhancement flags. */ export interface IKittyKeyboardState { - /** Current active enhancement flags */ + /** Current active enhancement flags (for current screen) */ flags: number; + /** Saved flags for main screen when alt is active */ + mainFlags: number; + /** Saved flags for alternate screen when main is active */ + altFlags: number; /** Stack of flags for main screen */ mainStack: number[]; /** Stack of flags for alternate screen */ diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 3bb64c53..e160e6e9 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -28,6 +28,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ const DEFAULT_KITTY_KEYBOARD_STATE = (): IKittyKeyboardState => ({ flags: 0, + mainFlags: 0, + altFlags: 0, mainStack: [], altStack: [] });