Files
NetMesh/components/terminal/runtime/kittyKeyboardProtocol.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

908 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});