Files
NetMesh/components/terminal/runtime/kittyKeyboardProtocol.test.ts

908 lines
31 KiB
TypeScript
Raw Permalink Normal View History

import test from "node:test";
import assert from "node:assert/strict";
import {
buildKittyKeyboardModeQueryResponse,
createKittyKeyboardModeState,
createKittyKeyboardSessionStateStore,
encodeKittyCompositionText,
encodeKittyKeyEvent,
encodeLegacyKeyboardEvent,
popKittyKeyboardModeFlags,
pushKittyKeyboardModeFlags,
restoreKittyKeyboardModeState,
setKittyKeyboardAlternateScreenActive,
setKittyKeyboardModeFlags,
snapshotKittyKeyboardModeState,
shouldExpectLegacyKeyboardData,
shouldMarkKittyTextInputEvent,
shouldTreatKittyAltAsText,
shouldTrackKittyKeyRelease,
} from "./kittyKeyboardProtocol";
import {
installKittyKeyboardProtocolHandlers,
installKittyKeyboardProtocolHandlersIfEnabled,
readKittyKeyboardCsiParam,
type KittyKeyboardCsiParams,
} from "./kittyKeyboardRuntime";
type CsiHandlerId = { prefix?: string; intermediates?: string; final: string };
type CsiHandler = (params: KittyKeyboardCsiParams) => boolean;
const csiKey = (id: CsiHandlerId): string => `${id.prefix ?? ""}|${id.intermediates ?? ""}|${id.final}`;
const createFakeCsiParser = () => {
const handlers = new Map<string, CsiHandler[]>();
return {
parser: {
registerCsiHandler(id: CsiHandlerId, callback: CsiHandler) {
const key = csiKey(id);
const list = handlers.get(key) ?? [];
list.push(callback);
handlers.set(key, list);
return { dispose: () => handlers.set(key, (handlers.get(key) ?? []).filter((item) => item !== callback)) };
},
registerEscHandler(id: { intermediates?: string; final: string }, callback: () => boolean) {
const key = `ESC|${id.intermediates ?? ""}|${id.final}`;
const list = handlers.get(key) ?? [];
list.push(callback);
handlers.set(key, list);
return { dispose: () => handlers.set(key, (handlers.get(key) ?? []).filter((item) => item !== callback)) };
},
},
dispatch(id: CsiHandlerId, params: KittyKeyboardCsiParams = []) {
const list = handlers.get(csiKey(id));
assert.ok(list?.length, `missing CSI handler for ${csiKey(id)}`);
for (let index = list.length - 1; index >= 0; index -= 1) {
if (list[index](params)) return true;
}
return false;
},
hasHandler(id: CsiHandlerId) {
return (handlers.get(csiKey(id))?.length ?? 0) > 0;
},
dispatchEsc(id: { intermediates?: string; final: string }) {
const list = handlers.get(`ESC|${id.intermediates ?? ""}|${id.final}`);
assert.ok(list?.length, `missing ESC handler for ${id.final}`);
return list.at(-1)!([]);
},
};
};
const withFlags = (flags: number) => {
const state = createKittyKeyboardModeState();
setKittyKeyboardModeFlags(state, flags);
return state;
};
const event = (key: string, overrides: Record<string, unknown> = {}) => ({
type: "keydown",
key,
code: key.length === 1 && /[a-z]/i.test(key) ? `Key${key.toUpperCase()}` : key,
getModifierState: () => false,
...overrides,
});
test("negotiates all five enhancement flags and masks unknown bits", () => {
const state = createKittyKeyboardModeState();
setKittyKeyboardModeFlags(state, 0xff);
assert.equal(buildKittyKeyboardModeQueryResponse(state), "\u001b[?31u");
setKittyKeyboardModeFlags(state, 8, 3);
assert.equal(buildKittyKeyboardModeQueryResponse(state), "\u001b[?23u");
setKittyKeyboardModeFlags(state, 8, 2);
assert.equal(buildKittyKeyboardModeQueryResponse(state), "\u001b[?31u");
});
test("maintains bounded independent main and alternate screen stacks", () => {
const state = createKittyKeyboardModeState();
setKittyKeyboardModeFlags(state, 1);
for (let flags = 0; flags < 40; flags += 1) pushKittyKeyboardModeFlags(state, flags);
assert.equal(state.mainStack.length, 32);
setKittyKeyboardAlternateScreenActive(state, true);
setKittyKeyboardModeFlags(state, 16);
pushKittyKeyboardModeFlags(state, 8);
assert.equal(popKittyKeyboardModeFlags(state), 16);
assert.equal(popKittyKeyboardModeFlags(state), 0);
setKittyKeyboardAlternateScreenActive(state, false);
assert.notEqual(buildKittyKeyboardModeQueryResponse(state), "\u001b[?16u");
});
test("snapshots and restores negotiated state across renderer handoffs", () => {
const source = createKittyKeyboardModeState();
setKittyKeyboardModeFlags(source, 1 | 8);
pushKittyKeyboardModeFlags(source, 2 | 4);
setKittyKeyboardAlternateScreenActive(source, true);
setKittyKeyboardModeFlags(source, 16);
pushKittyKeyboardModeFlags(source, 1 | 2 | 8);
const restored = createKittyKeyboardModeState();
restoreKittyKeyboardModeState(restored, snapshotKittyKeyboardModeState(source));
assert.deepEqual(restored, source);
restored.mainStack.push(31);
assert.notDeepEqual(restored, source);
});
test("disambiguates ambiguous ASCII and modified control keys", () => {
const state = withFlags(1);
assert.equal(encodeKittyKeyEvent(state, event("a")), null);
assert.equal(encodeKittyKeyEvent(state, event("Escape")), "\u001b[27u");
assert.equal(encodeKittyKeyEvent(state, event("c", { ctrlKey: true })), "\u001b[99;5u");
const disambiguate = withFlags(1);
assert.equal(
encodeKittyKeyEvent(disambiguate, event("c", {
ctrlKey: true,
getModifierState: (name: string) => name === "CapsLock" || name === "NumLock",
})),
"\u001b[99;197u",
);
assert.equal(encodeKittyKeyEvent(state, event("[", { code: "BracketLeft", altKey: true })), "\u001b[91;3u");
assert.equal(encodeKittyKeyEvent(state, event("Enter", { shiftKey: true })), "\u001b[13;2u");
});
test("alternate-key reporting alone never changes which events are encoded", () => {
const state = withFlags(4);
assert.equal(encodeKittyKeyEvent(state, event("Escape")), null);
assert.equal(encodeKittyKeyEvent(state, event("c", { ctrlKey: true })), null);
assert.equal(encodeKittyKeyEvent(state, event("Enter", { shiftKey: true })), "\r");
assert.equal(encodeKittyKeyEvent(state, event("Tab", { shiftKey: true })), "\u001b[Z");
assert.equal(
encodeKittyKeyEvent(state, event("с", { code: "KeyC", ctrlKey: true })),
"\u001b[1089::99;5u",
);
assert.equal(
encodeKittyKeyEvent(state, event("c", { code: "KeyJ", ctrlKey: true })),
"\u001b[99::106;5u",
);
assert.equal(
encodeKittyKeyEvent(state, event(";", { code: "KeyQ", ctrlKey: true })),
"\u001b[59::113;5u",
);
});
test("baseline mode preserves Ctrl+Shift combinations that legacy encoding loses", () => {
const state = withFlags(0);
assert.equal(
encodeKittyKeyEvent(state, event("I", { code: "KeyI", ctrlKey: true, shiftKey: true })),
"\u001b[105;6u",
);
assert.equal(encodeKittyKeyEvent(state, event("i", { ctrlKey: true })), null);
assert.equal(
encodeKittyKeyEvent(state, event(" ", { code: "Space", ctrlKey: true, shiftKey: true })),
"\0",
);
assert.equal(encodeKittyKeyEvent(state, event("F13")), "\u001b[57376u");
assert.equal(encodeKittyKeyEvent(state, event("F13", { repeat: true })), "\u001b[57376u");
assert.equal(encodeKittyKeyEvent(state, event("PrintScreen")), "\u001b[57361u");
assert.equal(encodeKittyKeyEvent(state, event("ContextMenu")), "\u001b[29~");
assert.equal(
encodeKittyKeyEvent(state, event("ContextMenu", { repeat: true })),
"\u001b[29~",
);
assert.equal(
encodeKittyKeyEvent(state, event("a", { metaKey: true })),
"\u001b[97;9u",
);
assert.equal(
encodeKittyKeyEvent(state, event("i", { ctrlKey: true, shiftKey: true, altKey: true })),
"\u001b[105;8u",
);
assert.equal(
encodeKittyKeyEvent(state, event(" ", {
code: "Space",
ctrlKey: true,
shiftKey: true,
getModifierState: (name: string) => name === "Hyper",
})),
"\u001b[32;22u",
);
assert.equal(
encodeKittyKeyEvent(state, event("Clear", { code: "Numpad5" })),
"\u001b[E",
);
assert.equal(
encodeKittyKeyEvent(state, event("Clear", {
code: "Numpad5",
applicationCursorMode: true,
})),
"\u001bOE",
);
assert.equal(
encodeKittyKeyEvent(state, event("Clear", {
code: "Numpad5",
ctrlKey: true,
applicationCursorMode: true,
})),
"\u001b[1;5E",
);
assert.equal(
encodeKittyKeyEvent(state, event("Tab", { altKey: true })),
"\u001b\t",
);
assert.equal(
encodeKittyKeyEvent(state, event("Tab", { ctrlKey: true, altKey: true })),
"\u001b\t",
);
assert.equal(
encodeKittyKeyEvent(state, event("Tab", { shiftKey: true, altKey: true })),
"\u001b\u001b[Z",
);
assert.equal(
encodeKittyKeyEvent(state, event("Tab", {
shiftKey: true,
ctrlKey: true,
altKey: true,
})),
"\u001b[9;8u",
);
assert.equal(
encodeKittyKeyEvent(state, event("Insert", { shiftKey: true })),
"\u001b[2;2~",
);
assert.equal(
encodeKittyKeyEvent(state, event("PageUp", { altKey: true })),
"\u001b[5;3~",
);
assert.equal(
encodeKittyKeyEvent(state, event("ArrowUp", { metaKey: true })),
"\u001b[1;9A",
);
assert.equal(
encodeKittyKeyEvent(state, event("ArrowUp", {
getModifierState: (name: string) => name === "Hyper",
})),
"\u001b[1;17A",
);
assert.equal(
encodeKittyKeyEvent(state, event("ArrowUp", {
getModifierState: (name: string) => name === "KittyMeta",
})),
"\u001b[1;33A",
);
const alternateOnly = withFlags(4);
assert.equal(encodeKittyKeyEvent(alternateOnly, event("F13")), "\u001b[57376u");
assert.equal(encodeKittyKeyEvent(alternateOnly, event("PrintScreen")), "\u001b[57361u");
assert.equal(encodeKittyKeyEvent(alternateOnly, event("ContextMenu")), "\u001b[29~");
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("ContextMenu", {
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[29;65~",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("Clear", {
code: "Numpad5",
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[1;65E",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("ArrowUp", {
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[1;65A",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("ArrowUp", {
altKey: true,
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[1;67A",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("Insert", {
shiftKey: true,
getModifierState: (name: string) => name === "NumLock",
})),
"\u001b[2;130~",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("Tab", {
altKey: true,
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b\t",
);
for (const [key, expected] of [
["Enter", "\u001b\r"],
["Backspace", "\u001b\u007f"],
["Escape", "\u001b\u001b"],
] as const) {
assert.equal(
encodeKittyKeyEvent(alternateOnly, event(key, {
altKey: true,
getModifierState: (name: string) => name === "CapsLock",
})),
expected,
);
}
for (const flags of [2, 4, 16]) {
for (const [lock, modifier] of [
["CapsLock", 69],
["NumLock", 133],
] as const) {
assert.equal(
encodeKittyKeyEvent(withFlags(flags), event("c", {
code: "KeyC",
ctrlKey: true,
getModifierState: (name: string) => name === lock,
})),
`\u001b[99;${modifier}u`,
);
}
}
assert.equal(
encodeKittyKeyEvent(alternateOnly, event("a", {
code: "KeyA",
altKey: true,
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[97;67u",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event(";", {
code: "Semicolon",
ctrlKey: true,
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[59;69u",
);
assert.equal(
encodeKittyKeyEvent(alternateOnly, event(" ", {
code: "Space",
ctrlKey: true,
getModifierState: (name: string) => name === "CapsLock",
})),
"\u001b[32;69u",
);
});
test("legacy fallback preserves text, controls, and application cursor mode", () => {
assert.equal(encodeLegacyKeyboardEvent(event("a")), "a");
assert.equal(encodeLegacyKeyboardEvent(event("c", { ctrlKey: true })), "\x03");
assert.equal(
encodeLegacyKeyboardEvent(event("3", { code: "Digit3", ctrlKey: true })),
"\u001b",
);
assert.equal(
encodeLegacyKeyboardEvent(event("c", { ctrlKey: true, altKey: true })),
"\u001b\x03",
);
assert.equal(encodeLegacyKeyboardEvent(event("ArrowUp"), true), "\u001bOA");
assert.equal(encodeLegacyKeyboardEvent(event("ArrowUp", { ctrlKey: true }), true), "\u001b[1;5A");
assert.equal(encodeLegacyKeyboardEvent(event("F5")), "\u001b[15~");
for (const [key, code] of [
[";", "Semicolon"],
["'", "Quote"],
[",", "Comma"],
[".", "Period"],
["0", "Digit0"],
["1", "Digit1"],
["9", "Digit9"],
] as const) {
assert.equal(encodeLegacyKeyboardEvent(event(key, { code, ctrlKey: true })), key);
}
assert.equal(
encodeLegacyKeyboardEvent(event(";", { code: "Semicolon", ctrlKey: true, altKey: true })),
"\u001b;",
);
});
test("baseline and event-type press preserve unmapped legacy Ctrl ASCII", () => {
for (const flags of [0, 2, 4, 16]) {
assert.equal(
encodeKittyKeyEvent(withFlags(flags), event(";", { code: "Semicolon", ctrlKey: true })),
";",
);
}
assert.equal(
encodeKittyKeyEvent(withFlags(1), event(";", { code: "Semicolon", ctrlKey: true })),
"\u001b[59;5u",
);
for (const [key, code, expected] of [
["ж", "Semicolon", ";"],
["э", "Quote", "'"],
["б", "Comma", ","],
["ю", "Period", "."],
] as const) {
assert.equal(
encodeKittyKeyEvent(withFlags(0), event(key, { code, ctrlKey: true })),
expected,
);
assert.equal(
encodeKittyKeyEvent(withFlags(2), event(key, { code, ctrlKey: true })),
expected,
);
assert.equal(
encodeLegacyKeyboardEvent(event(key, { code, ctrlKey: true })),
expected,
);
}
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("c", { code: "KeyJ", ctrlKey: true })),
"\x03",
);
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("c", {
code: "KeyJ",
ctrlKey: true,
altKey: true,
})),
"\u001b\x03",
);
assert.equal(
encodeLegacyKeyboardEvent(event("c", { code: "KeyJ", ctrlKey: true })),
"\x03",
);
assert.equal(
encodeLegacyKeyboardEvent(event("3", { code: "Semicolon", ctrlKey: true })),
"\u001b",
);
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("j", { code: "KeyC", ctrlKey: true })),
"\x0a",
);
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("j", {
code: "KeyC",
ctrlKey: true,
altKey: true,
})),
"\u001b\x0a",
);
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("a", {
code: "IntlBackslash",
keyCode: 226,
ctrlKey: true,
})),
"\x01",
);
assert.equal(
encodeKittyKeyEvent(withFlags(2), event("a", {
code: "IntlBackslash",
keyCode: 226,
ctrlKey: true,
altKey: true,
})),
"\u001b\x01",
);
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("ж", {
code: "IntlBackslash",
keyCode: 226,
ctrlKey: true,
})),
"\u001b[1078;5u",
);
assert.equal(
encodeKittyKeyEvent(withFlags(2), event("ж", {
code: "IntlBackslash",
keyCode: 226,
ctrlKey: true,
altKey: true,
})),
"\u001b[1078;7u",
);
});
test("macOS Option text does not depend on the asynchronous layout map", () => {
assert.equal(shouldTreatKittyAltAsText({ key: "å", altKey: true }, true, false), true);
assert.equal(shouldTreatKittyAltAsText({ key: "a", altKey: true }, true, false), true);
assert.equal(shouldTreatKittyAltAsText({ key: "Dead", altKey: true }, true, false), true);
assert.equal(shouldTreatKittyAltAsText({ key: "ArrowLeft", altKey: true }, true, false), false);
assert.equal(shouldTreatKittyAltAsText({ key: "å", altKey: true }, true, true), false);
assert.equal(shouldTreatKittyAltAsText({ key: "Dead", altKey: true }, true, true), false);
assert.equal(shouldTreatKittyAltAsText({ key: "å", altKey: true }, false, false), false);
});
test("macOS Option-as-Meta encodes physical dead keys while text-producing Option defers", () => {
const state = withFlags(1 | 2);
const optionN = event("Dead", {
code: "KeyN",
altKey: true,
altKeyProducesText: false,
});
assert.equal(encodeKittyKeyEvent(state, optionN), "\u001b[110;3u");
assert.equal(
encodeKittyKeyEvent(state, { ...optionN, type: "keyup" }),
"\u001b[110;3:3u",
);
assert.equal(
encodeKittyKeyEvent(state, { ...optionN, altKeyProducesText: true }),
null,
);
assert.equal(
encodeKittyKeyEvent(state, {
...optionN,
ctrlKey: true,
getModifierState: (name: string) => name === "AltGraph",
}),
null,
);
});
test("baseline modes leave shifted AltGraph text to the browser input path", () => {
const shiftedAltGraph = event("€", {
code: "KeyE",
ctrlKey: true,
altKey: true,
shiftKey: true,
getModifierState: (name: string) => name === "AltGraph",
});
assert.equal(encodeKittyKeyEvent(withFlags(0), shiftedAltGraph), null);
assert.equal(encodeKittyKeyEvent(withFlags(4), shiftedAltGraph), null);
});
test("legacy broadcast pairing ignores Meta shortcuts that produce no terminal data", () => {
assert.equal(shouldExpectLegacyKeyboardData(event("a", { metaKey: true })), false);
assert.equal(shouldExpectLegacyKeyboardData(event("a")), true);
assert.equal(shouldExpectLegacyKeyboardData(event("Enter")), true);
});
test("reports press, repeat, and release only for eligible keys", () => {
const state = withFlags(2);
assert.equal(encodeKittyKeyEvent(state, event("Escape")), null);
for (const [overrides, modifier] of [
[{ shiftKey: true }, 2],
[{ altKey: true }, 3],
[{ ctrlKey: true }, 5],
[{ metaKey: true }, 9],
[{ getModifierState: (name: string) => name === "CapsLock" }, 65],
[{ getModifierState: (name: string) => name === "NumLock" }, 129],
] as const) {
assert.equal(
encodeKittyKeyEvent(state, event("Escape", overrides)),
`\u001b[27;${modifier}u`,
);
}
assert.equal(
encodeKittyKeyEvent(state, event(" ", {
code: "Space",
ctrlKey: true,
shiftKey: true,
})),
"\0",
);
assert.equal(
encodeKittyKeyEvent(state, event(" ", {
code: "Space",
ctrlKey: true,
shiftKey: true,
repeat: true,
})),
"\u001b[32;6:2u",
);
assert.equal(
encodeKittyKeyEvent(state, event(" ", {
type: "keyup",
code: "Space",
ctrlKey: true,
shiftKey: true,
})),
"\u001b[32;6:3u",
);
assert.equal(encodeKittyKeyEvent(state, event("ArrowUp")), "\u001b[A");
assert.equal(encodeKittyKeyEvent(state, event("ArrowUp", { repeat: true })), "\u001b[1;1:2A");
assert.equal(encodeKittyKeyEvent(state, event("ArrowUp", { type: "keyup" })), "\u001b[1;1:3A");
assert.equal(encodeKittyKeyEvent(state, event("a", { repeat: true })), null);
assert.equal(
encodeKittyKeyEvent(state, event("A", { code: "KeyA", shiftKey: true, repeat: true })),
null,
);
assert.equal(encodeKittyKeyEvent(state, event("a", { type: "keyup" })), "\u001b[97;1:3u");
assert.equal(
encodeKittyKeyEvent(state, event("A", { code: "KeyA", shiftKey: true, type: "keyup" })),
"\u001b[97;2:3u",
);
assert.equal(encodeKittyKeyEvent(state, event("Enter", { type: "keyup" })), null);
assert.equal(encodeKittyKeyEvent(state, event("c", { ctrlKey: true })), null);
assert.equal(
encodeKittyKeyEvent(state, event("c", { ctrlKey: true, repeat: true })),
"\u001b[99;5:2u",
);
assert.equal(
encodeKittyKeyEvent(state, event("c", { type: "keyup", ctrlKey: true })),
"\u001b[99;5:3u",
);
assert.equal(
encodeKittyKeyEvent(state, event(" ", { code: "Space", ctrlKey: true })),
null,
);
assert.equal(
encodeKittyKeyEvent(state, event(" ", { code: "Space", ctrlKey: true, repeat: true })),
"\u001b[32;5:2u",
);
assert.equal(
encodeKittyKeyEvent(state, event(" ", { code: "Space", ctrlKey: true, type: "keyup" })),
"\u001b[32;5:3u",
);
assert.equal(encodeKittyKeyEvent(state, event("Enter", { repeat: true })), null);
assert.equal(encodeKittyKeyEvent(state, event("Tab", { repeat: true })), null);
assert.equal(encodeKittyKeyEvent(state, event("Backspace", { repeat: true })), null);
assert.equal(shouldTrackKittyKeyRelease(state, event("c", { ctrlKey: true })), true);
assert.equal(shouldTrackKittyKeyRelease(state, event("Enter")), false);
});
test("event-type mode matches Kitty's printable-key release behavior", () => {
const state = withFlags(2);
assert.equal(encodeKittyKeyEvent(state, event("a")), null);
assert.equal(encodeKittyKeyEvent(state, event("a", { repeat: true })), null);
assert.equal(
encodeKittyKeyEvent(state, event("a", { type: "keyup" })),
"\u001b[97;1:3u",
);
assert.equal(
encodeKittyKeyEvent(state, event("A", { code: "KeyA", shiftKey: true, type: "keyup" })),
"\u001b[97;2:3u",
);
});
test("reports alternate shifted and PC-101 layout key values", () => {
const state = withFlags(1 | 4);
assert.equal(
encodeKittyKeyEvent(state, event("+", { code: "Equal", shiftKey: true, ctrlKey: true })),
"\u001b[61:43;6u",
);
assert.equal(
encodeKittyKeyEvent(state, event("с", { code: "KeyC", ctrlKey: true })),
"\u001b[1089::99;5u",
);
});
test("report-all encodes text, controls, modifiers, repeat, and release", () => {
const state = withFlags(8);
assert.equal(encodeKittyKeyEvent(state, event("a")), "\u001b[97u");
assert.equal(encodeKittyKeyEvent(state, event("Enter")), "\u001b[13u");
assert.equal(
encodeKittyKeyEvent(state, event("Shift", { code: "ShiftLeft", shiftKey: true })),
"\u001b[57441;2u",
);
setKittyKeyboardModeFlags(state, 2, 2);
assert.equal(encodeKittyKeyEvent(state, event("a", { repeat: true })), "\u001b[97;1:2u");
assert.equal(encodeKittyKeyEvent(state, event("a", { type: "keyup" })), "\u001b[97;1:3u");
assert.equal(encodeKittyKeyEvent(state, event("Enter", { type: "keyup" })), "\u001b[13;1:3u");
});
test("associated text supports multiple code points and pure composition text", () => {
const state = withFlags(8 | 16);
assert.equal(
encodeKittyKeyEvent(state, event("A", { code: "KeyA", shiftKey: true })),
"\u001b[97;2;65u",
);
assert.equal(encodeKittyKeyEvent(state, event("Enter")), "\u001b[13u");
assert.equal(encodeKittyKeyEvent(state, event("F13")), "\u001b[57376u");
assert.equal(
encodeKittyKeyEvent(state, event("7", { code: "Numpad7" })),
"\u001b[57406;;55u",
);
assert.equal(
encodeKittyKeyEvent(state, event("+", { code: "NumpadAdd" })),
"\u001b[57413;;43u",
);
assert.equal(encodeKittyKeyEvent(state, event("Dead", { code: "Quote" })), null);
assert.equal(
encodeKittyKeyEvent(state, event("é", { code: "KeyE" })),
"\u001b[101;;101:769u",
);
assert.equal(encodeKittyCompositionText(state, "你😀"), "\u001b[0;;20320:128512u");
assert.equal(encodeKittyCompositionText(state, "\n\u0085"), null);
});
test("report-all without associated text leaves composition text for literal fallback", () => {
assert.equal(encodeKittyCompositionText(withFlags(8), "你"), null);
assert.equal(encodeKittyCompositionText(withFlags(8), ""), null);
});
test("encodes the complete functional, keypad, media, and modifier ranges", () => {
const state = withFlags(8);
const cases = [
[event("F13"), "\u001b[57376u"],
[event("F35"), "\u001b[57398u"],
[event("7", { code: "Numpad7" }), "\u001b[57406u"],
[event("Home", { code: "Numpad7" }), "\u001b[57423u"],
[event("Delete", { code: "NumpadDecimal" }), "\u001b[57426u"],
[event("Clear", { code: "Numpad5" }), "\u001b[57427~"],
[event(",", { code: "NumpadComma" }), "\u001b[57416u"],
[event("MediaPlay"), "\u001b[57428u"],
[event("MediaRecord"), "\u001b[57437u"],
[event("AudioVolumeMute"), "\u001b[57440u"],
[event("ContextMenu"), "\u001b[57363u"],
[event("Control", { code: "ControlRight", ctrlKey: true }), "\u001b[57448;5u"],
[event("ISOLevel5Shift"), "\u001b[57454u"],
] as const;
for (const [input, expected] of cases) assert.equal(encodeKittyKeyEvent(state, input), expected);
});
test("uses official enhanced F-key encodings and covers every F-key code", () => {
const state = withFlags(8);
assert.equal(encodeKittyKeyEvent(state, event("F1")), "\u001b[P");
assert.equal(encodeKittyKeyEvent(state, event("F2")), "\u001b[Q");
assert.equal(encodeKittyKeyEvent(state, event("F3")), "\u001b[13~");
assert.equal(encodeKittyKeyEvent(state, event("F4")), "\u001b[S");
assert.equal(
encodeKittyKeyEvent(state, event("F3", { ctrlKey: true })),
"\u001b[13;5~",
);
const legacyTilde = [15, 17, 18, 19, 20, 21, 23, 24];
for (let number = 5; number <= 12; number += 1) {
assert.equal(encodeKittyKeyEvent(state, event(`F${number}`)), `\u001b[${legacyTilde[number - 5]}~`);
}
for (let number = 13; number <= 35; number += 1) {
assert.equal(encodeKittyKeyEvent(state, event(`F${number}`)), `\u001b[${57363 + number}u`);
}
});
test("uses the F3 tilde form for modified baseline events", () => {
const state = withFlags(0);
assert.equal(encodeKittyKeyEvent(state, event("F3", { ctrlKey: true })), "\u001b[13;5~");
assert.equal(encodeKittyKeyEvent(state, event("F3", { shiftKey: true })), "\u001b[13;2~");
assert.equal(encodeKittyKeyEvent(state, event("F3", { altKey: true })), "\u001b[13;3~");
});
test("uses application cursor mode only for unmodified cursor keys", () => {
const state = withFlags(8);
assert.equal(
encodeKittyKeyEvent(state, event("ArrowUp", { applicationCursorMode: true })),
"\u001b[A",
);
assert.equal(
encodeKittyKeyEvent(state, event("Home", { applicationCursorMode: true })),
"\u001b[H",
);
assert.equal(
encodeKittyKeyEvent(state, event("End", { applicationCursorMode: true })),
"\u001b[F",
);
assert.equal(
encodeKittyKeyEvent(state, event("ArrowUp", {
applicationCursorMode: true,
ctrlKey: true,
})),
"\u001b[1;5A",
);
});
test("recognizes xterm input-only text without treating paste as a key event", () => {
assert.equal(shouldMarkKittyTextInputEvent({ data: "😀", inputType: "insertText" }), true);
assert.equal(shouldMarkKittyTextInputEvent({ data: "hello", inputType: "insertFromPaste" }), false);
assert.equal(shouldMarkKittyTextInputEvent({ data: null, inputType: "insertText" }), false);
});
test("preserves negotiated state only for renderer hibernation", () => {
const store = createKittyKeyboardSessionStateStore();
const sessionOwner = {};
const initial = store.resolve(sessionOwner, false);
setKittyKeyboardModeFlags(initial, 31);
setKittyKeyboardAlternateScreenActive(initial, true);
pushKittyKeyboardModeFlags(initial, 8);
const awakened = store.resolve(sessionOwner, true);
assert.equal(awakened, initial);
assert.equal(awakened.alternateScreenActive, true);
assert.equal(awakened.mainFlags, 31);
assert.deepEqual(awakened.alternateStack, [0]);
const reconnected = store.resolve(sessionOwner, false);
assert.notEqual(reconnected, initial);
assert.equal(buildKittyKeyboardModeQueryResponse(reconnected), "\u001b[?0u");
assert.notEqual(store.resolve({}, true), reconnected);
setKittyKeyboardModeFlags(reconnected, 8);
store.reset(sessionOwner);
assert.equal(buildKittyKeyboardModeQueryResponse(reconnected), "\u001b[?0u");
});
test("includes lock modifiers and excludes associated control text", () => {
const state = withFlags(8 | 16);
assert.equal(
encodeKittyKeyEvent(state, event("a", {
getModifierState: (name: string) => name === "CapsLock" || name === "NumLock",
})),
"\u001b[97;193;97u",
);
const altGraph = withFlags(1);
assert.equal(
encodeKittyKeyEvent(altGraph, event("@", {
code: "KeyQ",
ctrlKey: true,
altKey: true,
unshiftedKey: "q",
getModifierState: (name: string) => name === "AltGraph",
})),
null,
);
assert.equal(
encodeKittyKeyEvent(altGraph, event("å", {
code: "KeyA",
altKey: true,
unshiftedKey: "a",
altKeyProducesText: true,
})),
null,
);
assert.equal(encodeKittyKeyEvent(state, event("c", { ctrlKey: true })), "\u001b[99;5u");
assert.equal(
encodeKittyKeyEvent(state, event("a", {
getModifierState: (name: string) => name === "Hyper" || name === "KittyMeta",
})),
"\u001b[97;49u",
);
assert.equal(encodeKittyKeyEvent(state, event("a", { metaKey: true })), "\u001b[97;9u");
});
test("baseline protocol excludes lock state from Ctrl+Shift disambiguation", () => {
assert.equal(
encodeKittyKeyEvent(withFlags(0), event("I", {
code: "KeyI",
ctrlKey: true,
shiftKey: true,
getModifierState: (name: string) => name === "CapsLock" || name === "NumLock",
})),
"\u001b[105;6u",
);
});
test("disambiguation leaves text-producing keypad keys on their text path", () => {
assert.equal(
encodeKittyKeyEvent(withFlags(1), event("7", { code: "Numpad7" })),
null,
);
assert.equal(
encodeKittyKeyEvent(withFlags(1), event("Home", { code: "Numpad7" })),
"\u001b[57423u",
);
assert.equal(
encodeKittyKeyEvent(withFlags(1), event("7", {
code: "Numpad7",
ctrlKey: true,
})),
"\u001b[57406;5u",
);
});
test("keypad begin preserves baseline and event-type forms", () => {
const state = withFlags(2);
assert.equal(encodeKittyKeyEvent(state, event("Clear", { code: "Numpad5" })), "\u001b[E");
assert.equal(
encodeKittyKeyEvent(state, event("Clear", { code: "Numpad5", repeat: true })),
"\u001b[1;1:2E",
);
assert.equal(
encodeKittyKeyEvent(state, event("Clear", { code: "Numpad5", type: "keyup" })),
"\u001b[1;1:3E",
);
});
test("CSI handlers negotiate, query, stack, and track alternate screen", () => {
const state = createKittyKeyboardModeState();
const fake = createFakeCsiParser();
const replies: string[] = [];
const disposable = installKittyKeyboardProtocolHandlers(fake.parser, state, (payload) => replies.push(payload));
fake.dispatch({ prefix: "=", final: "u" }, [31]);
fake.dispatch({ prefix: "?", final: "u" });
assert.equal(replies.at(-1), "\u001b[?31u");
assert.equal(fake.dispatchEsc({ final: "c" }), false);
fake.dispatch({ prefix: "?", final: "u" });
assert.equal(replies.at(-1), "\u001b[?0u");
fake.dispatch({ prefix: "=", final: "u" }, [31]);
fake.dispatch({ prefix: ">", final: "u" }, [1]);
fake.dispatch({ prefix: "<", final: "u" });
assert.equal(fake.dispatch({ prefix: "?", final: "h" }, [1049]), false);
fake.dispatch({ prefix: "=", final: "u" }, [8]);
assert.equal(fake.dispatch({ prefix: "?", final: "l" }, [[1049]]), false);
fake.dispatch({ prefix: "?", final: "u" });
assert.equal(replies.at(-1), "\u001b[?31u");
disposable.dispose();
assert.equal(fake.hasHandler({ prefix: "?", final: "u" }), false);
});
test("CSI parser helpers retain explicit opt-in policy", () => {
assert.equal(readKittyKeyboardCsiParam([], 0, 7), 7);
assert.equal(readKittyKeyboardCsiParam([[8, 9]], 0, 7), 8);
const fake = createFakeCsiParser();
const state = createKittyKeyboardModeState();
assert.equal(installKittyKeyboardProtocolHandlersIfEnabled(false, fake.parser, state, () => {}), undefined);
assert.equal(fake.hasHandler({ prefix: "?", final: "u" }), false);
});