Maintain different buffer flags, stack limit, tests

This commit is contained in:
Daniel Imms
2026-01-09 11:59:50 -08:00
parent 5a29d489c3
commit c624a47780
5 changed files with 122 additions and 48 deletions
+96 -47
View File
@@ -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']);
});
});
+17
View File
@@ -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;
+2
View File
@@ -116,6 +116,8 @@ export class MockCoreService implements ICoreService {
};
public kittyKeyboard = {
flags: 0,
mainFlags: 0,
altFlags: 0,
mainStack: [] as number[],
altStack: [] as number[]
};
+5 -1
View File
@@ -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 */
+2
View File
@@ -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: []
});