/** * Tests for keyboard input handling */ import assert from "node:assert"; import { describe, it } from "node:test"; import { matchesKey, parseKey, setKittyProtocolActive } from "../src/keys.js"; describe("matchesKey", () => { describe("Kitty protocol with alternate keys (non-Latin layouts)", () => { // Kitty protocol flag 4 (Report alternate keys) sends: // CSI codepoint:shifted:base ; modifier:event u // Where base is the key in standard PC-101 layout it("should match Ctrl+c when pressing Ctrl+С (Cyrillic) with base layout key", () => { setKittyProtocolActive(true); // Cyrillic 'с' = codepoint 1089, Latin 'c' = codepoint 99 // Format: CSI 1089::99;5u (codepoint::base;modifier with ctrl=4, +1=5) const cyrillicCtrlC = "\x1b[1089::99;5u"; assert.strictEqual(matchesKey(cyrillicCtrlC, "ctrl+c"), true); setKittyProtocolActive(false); }); it("should match Ctrl+d when pressing Ctrl+В (Cyrillic) with base layout key", () => { setKittyProtocolActive(true); // Cyrillic 'в' = codepoint 1074, Latin 'd' = codepoint 100 const cyrillicCtrlD = "\x1b[1074::100;5u"; assert.strictEqual(matchesKey(cyrillicCtrlD, "ctrl+d"), true); setKittyProtocolActive(false); }); it("should match Ctrl+z when pressing Ctrl+Я (Cyrillic) with base layout key", () => { setKittyProtocolActive(true); // Cyrillic 'я' = codepoint 1103, Latin 'z' = codepoint 122 const cyrillicCtrlZ = "\x1b[1103::122;5u"; assert.strictEqual(matchesKey(cyrillicCtrlZ, "ctrl+z"), true); setKittyProtocolActive(false); }); it("should match Ctrl+Shift+p with base layout key", () => { setKittyProtocolActive(true); // Cyrillic 'з' = codepoint 1079, Latin 'p' = codepoint 112 // ctrl=4, shift=1, +1 = 6 const cyrillicCtrlShiftP = "\x1b[1079::112;6u"; assert.strictEqual(matchesKey(cyrillicCtrlShiftP, "ctrl+shift+p"), true); setKittyProtocolActive(false); }); it("should still match direct codepoint when no base layout key", () => { setKittyProtocolActive(true); // Latin ctrl+c without base layout key (terminal doesn't support flag 4) const latinCtrlC = "\x1b[99;5u"; assert.strictEqual(matchesKey(latinCtrlC, "ctrl+c"), true); setKittyProtocolActive(false); }); it("should handle shifted key in format", () => { setKittyProtocolActive(true); // Format with shifted key: CSI codepoint:shifted:base;modifier u // Latin 'c' with shifted 'C' (67) and base 'c' (99) const shiftedKey = "\x1b[99:67:99;2u"; // shift modifier = 1, +1 = 2 assert.strictEqual(matchesKey(shiftedKey, "shift+c"), true); setKittyProtocolActive(false); }); it("should handle event type in format", () => { setKittyProtocolActive(true); // Format with event type: CSI codepoint::base;modifier:event u // Cyrillic ctrl+c release event (event type 3) const releaseEvent = "\x1b[1089::99;5:3u"; assert.strictEqual(matchesKey(releaseEvent, "ctrl+c"), true); setKittyProtocolActive(false); }); it("should handle full format with shifted key, base key, and event type", () => { setKittyProtocolActive(true); // Full format: CSI codepoint:shifted:base;modifier:event u // Cyrillic 'С' (shifted) with base 'c', Ctrl+Shift pressed, repeat event // Cyrillic 'с' = 1089, Cyrillic 'С' = 1057, Latin 'c' = 99 // ctrl=4, shift=1, +1 = 6, repeat event = 2 const fullFormat = "\x1b[1089:1057:99;6:2u"; assert.strictEqual(matchesKey(fullFormat, "ctrl+shift+c"), true); setKittyProtocolActive(false); }); it("should prefer codepoint for Latin letters even when base layout differs", () => { setKittyProtocolActive(true); // Dvorak Ctrl+K reports codepoint 'k' (107) and base layout 'v' (118) const dvorakCtrlK = "\x1b[107::118;5u"; assert.strictEqual(matchesKey(dvorakCtrlK, "ctrl+k"), true); assert.strictEqual(matchesKey(dvorakCtrlK, "ctrl+v"), false); setKittyProtocolActive(false); }); it("should prefer codepoint for symbol keys even when base layout differs", () => { setKittyProtocolActive(true); // Dvorak Ctrl+/ reports codepoint '/' (47) and base layout '[' (91) const dvorakCtrlSlash = "\x1b[47::91;5u"; assert.strictEqual(matchesKey(dvorakCtrlSlash, "ctrl+/"), true); assert.strictEqual(matchesKey(dvorakCtrlSlash, "ctrl+["), false); setKittyProtocolActive(false); }); it("should not match wrong key even with base layout", () => { setKittyProtocolActive(true); // Cyrillic ctrl+с with base 'c' should NOT match ctrl+d const cyrillicCtrlC = "\x1b[1089::99;5u"; assert.strictEqual(matchesKey(cyrillicCtrlC, "ctrl+d"), false); setKittyProtocolActive(false); }); it("should not match wrong modifiers even with base layout", () => { setKittyProtocolActive(true); // Cyrillic ctrl+с should NOT match ctrl+shift+c const cyrillicCtrlC = "\x1b[1089::99;5u"; assert.strictEqual(matchesKey(cyrillicCtrlC, "ctrl+shift+c"), false); setKittyProtocolActive(false); }); }); describe("Legacy key matching", () => { it("should match legacy Ctrl+c", () => { setKittyProtocolActive(false); // Ctrl+c sends ASCII 3 (ETX) assert.strictEqual(matchesKey("\x03", "ctrl+c"), true); }); it("should match legacy Ctrl+d", () => { setKittyProtocolActive(false); // Ctrl+d sends ASCII 4 (EOT) assert.strictEqual(matchesKey("\x04", "ctrl+d"), true); }); it("should match escape key", () => { assert.strictEqual(matchesKey("\x1b", "escape"), true); }); it("should match legacy linefeed as enter", () => { setKittyProtocolActive(false); assert.strictEqual(matchesKey("\n", "enter"), true); assert.strictEqual(parseKey("\n"), "enter"); }); it("should treat linefeed as shift+enter when kitty active", () => { setKittyProtocolActive(true); assert.strictEqual(matchesKey("\n", "shift+enter"), true); assert.strictEqual(matchesKey("\n", "enter"), false); assert.strictEqual(parseKey("\n"), "shift+enter"); setKittyProtocolActive(false); }); it("should parse ctrl+space", () => { setKittyProtocolActive(false); assert.strictEqual(matchesKey("\x00", "ctrl+space"), true); assert.strictEqual(parseKey("\x00"), "ctrl+space"); }); it("should match legacy Ctrl+symbol", () => { setKittyProtocolActive(false); // Ctrl+\ sends ASCII 28 (File Separator) in legacy terminals assert.strictEqual(matchesKey("\x1c", "ctrl+\\"), true); assert.strictEqual(parseKey("\x1c"), "ctrl+\\"); // Ctrl+] sends ASCII 29 (Group Separator) in legacy terminals assert.strictEqual(matchesKey("\x1d", "ctrl+]"), true); assert.strictEqual(parseKey("\x1d"), "ctrl+]"); // Ctrl+_ sends ASCII 31 (Unit Separator) in legacy terminals // Ctrl+- is on the same physical key on US keyboards assert.strictEqual(matchesKey("\x1f", "ctrl+_"), true); assert.strictEqual(matchesKey("\x1f", "ctrl+-"), true); assert.strictEqual(parseKey("\x1f"), "ctrl+-"); }); it("should match legacy Ctrl+Alt+symbol", () => { setKittyProtocolActive(false); // Ctrl+Alt+[ sends ESC followed by ESC (Ctrl+[ = ESC) assert.strictEqual(matchesKey("\x1b\x1b", "ctrl+alt+["), true); assert.strictEqual(parseKey("\x1b\x1b"), "ctrl+alt+["); // Ctrl+Alt+\ sends ESC followed by ASCII 28 assert.strictEqual(matchesKey("\x1b\x1c", "ctrl+alt+\\"), true); assert.strictEqual(parseKey("\x1b\x1c"), "ctrl+alt+\\"); // Ctrl+Alt+] sends ESC followed by ASCII 29 assert.strictEqual(matchesKey("\x1b\x1d", "ctrl+alt+]"), true); assert.strictEqual(parseKey("\x1b\x1d"), "ctrl+alt+]"); // Ctrl+_ sends ASCII 31 (Unit Separator) in legacy terminals // Ctrl+- is on the same physical key on US keyboards assert.strictEqual(matchesKey("\x1b\x1f", "ctrl+alt+_"), true); assert.strictEqual(matchesKey("\x1b\x1f", "ctrl+alt+-"), true); assert.strictEqual(parseKey("\x1b\x1f"), "ctrl+alt+-"); }); it("should parse legacy alt-prefixed sequences when kitty inactive", () => { setKittyProtocolActive(false); assert.strictEqual(matchesKey("\x1b ", "alt+space"), true); assert.strictEqual(parseKey("\x1b "), "alt+space"); assert.strictEqual(matchesKey("\x1b\b", "alt+backspace"), true); assert.strictEqual(parseKey("\x1b\b"), "alt+backspace"); assert.strictEqual(matchesKey("\x1b\x03", "ctrl+alt+c"), true); assert.strictEqual(parseKey("\x1b\x03"), "ctrl+alt+c"); assert.strictEqual(matchesKey("\x1bB", "alt+left"), true); assert.strictEqual(parseKey("\x1bB"), "alt+left"); assert.strictEqual(matchesKey("\x1bF", "alt+right"), true); assert.strictEqual(parseKey("\x1bF"), "alt+right"); assert.strictEqual(matchesKey("\x1ba", "alt+a"), true); assert.strictEqual(parseKey("\x1ba"), "alt+a"); assert.strictEqual(matchesKey("\x1by", "alt+y"), true); assert.strictEqual(parseKey("\x1by"), "alt+y"); assert.strictEqual(matchesKey("\x1bz", "alt+z"), true); assert.strictEqual(parseKey("\x1bz"), "alt+z"); setKittyProtocolActive(true); assert.strictEqual(matchesKey("\x1b ", "alt+space"), false); assert.strictEqual(parseKey("\x1b "), undefined); assert.strictEqual(matchesKey("\x1b\b", "alt+backspace"), true); assert.strictEqual(parseKey("\x1b\b"), "alt+backspace"); assert.strictEqual(matchesKey("\x1b\x03", "ctrl+alt+c"), false); assert.strictEqual(parseKey("\x1b\x03"), undefined); assert.strictEqual(matchesKey("\x1bB", "alt+left"), false); assert.strictEqual(parseKey("\x1bB"), undefined); assert.strictEqual(matchesKey("\x1bF", "alt+right"), false); assert.strictEqual(parseKey("\x1bF"), undefined); assert.strictEqual(matchesKey("\x1ba", "alt+a"), false); assert.strictEqual(parseKey("\x1ba"), undefined); assert.strictEqual(matchesKey("\x1by", "alt+y"), false); assert.strictEqual(parseKey("\x1by"), undefined); setKittyProtocolActive(false); }); it("should match arrow keys", () => { assert.strictEqual(matchesKey("\x1b[A", "up"), true); assert.strictEqual(matchesKey("\x1b[B", "down"), true); assert.strictEqual(matchesKey("\x1b[C", "right"), true); assert.strictEqual(matchesKey("\x1b[D", "left"), true); }); it("should match SS3 arrows and home/end", () => { assert.strictEqual(matchesKey("\x1bOA", "up"), true); assert.strictEqual(matchesKey("\x1bOB", "down"), true); assert.strictEqual(matchesKey("\x1bOC", "right"), true); assert.strictEqual(matchesKey("\x1bOD", "left"), true); assert.strictEqual(matchesKey("\x1bOH", "home"), true); assert.strictEqual(matchesKey("\x1bOF", "end"), true); }); it("should match legacy function keys and clear", () => { assert.strictEqual(matchesKey("\x1bOP", "f1"), true); assert.strictEqual(matchesKey("\x1b[24~", "f12"), true); assert.strictEqual(matchesKey("\x1b[E", "clear"), true); }); it("should match alt+arrows", () => { assert.strictEqual(matchesKey("\x1bp", "alt+up"), true); assert.strictEqual(matchesKey("\x1bp", "up"), false); }); it("should match rxvt modifier sequences", () => { assert.strictEqual(matchesKey("\x1b[a", "shift+up"), true); assert.strictEqual(matchesKey("\x1bOa", "ctrl+up"), true); assert.strictEqual(matchesKey("\x1b[2$", "shift+insert"), true); assert.strictEqual(matchesKey("\x1b[2^", "ctrl+insert"), true); assert.strictEqual(matchesKey("\x1b[7$", "shift+home"), true); }); }); }); describe("parseKey", () => { describe("Kitty protocol with alternate keys", () => { it("should return Latin key name when base layout key is present", () => { setKittyProtocolActive(true); // Cyrillic ctrl+с with base layout 'c' const cyrillicCtrlC = "\x1b[1089::99;5u"; assert.strictEqual(parseKey(cyrillicCtrlC), "ctrl+c"); setKittyProtocolActive(false); }); it("should prefer codepoint for Latin letters when base layout differs", () => { setKittyProtocolActive(true); // Dvorak Ctrl+K reports codepoint 'k' (107) and base layout 'v' (118) const dvorakCtrlK = "\x1b[107::118;5u"; assert.strictEqual(parseKey(dvorakCtrlK), "ctrl+k"); setKittyProtocolActive(false); }); it("should prefer codepoint for symbol keys when base layout differs", () => { setKittyProtocolActive(true); // Dvorak Ctrl+/ reports codepoint '/' (47) and base layout '[' (91) const dvorakCtrlSlash = "\x1b[47::91;5u"; assert.strictEqual(parseKey(dvorakCtrlSlash), "ctrl+/"); setKittyProtocolActive(false); }); it("should return key name from codepoint when no base layout", () => { setKittyProtocolActive(true); const latinCtrlC = "\x1b[99;5u"; assert.strictEqual(parseKey(latinCtrlC), "ctrl+c"); setKittyProtocolActive(false); }); it("should ignore Kitty CSI-u with unsupported modifiers", () => { setKittyProtocolActive(true); assert.strictEqual(parseKey("\x1b[99;9u"), undefined); setKittyProtocolActive(false); }); }); describe("Legacy key parsing", () => { it("should parse legacy Ctrl+letter", () => { setKittyProtocolActive(false); assert.strictEqual(parseKey("\x03"), "ctrl+c"); assert.strictEqual(parseKey("\x04"), "ctrl+d"); }); it("should parse special keys", () => { assert.strictEqual(parseKey("\x1b"), "escape"); assert.strictEqual(parseKey("\t"), "tab"); assert.strictEqual(parseKey("\r"), "enter"); assert.strictEqual(parseKey("\n"), "enter"); assert.strictEqual(parseKey("\x00"), "ctrl+space"); assert.strictEqual(parseKey(" "), "space"); }); it("should parse arrow keys", () => { assert.strictEqual(parseKey("\x1b[A"), "up"); assert.strictEqual(parseKey("\x1b[B"), "down"); assert.strictEqual(parseKey("\x1b[C"), "right"); assert.strictEqual(parseKey("\x1b[D"), "left"); }); it("should parse SS3 arrows and home/end", () => { assert.strictEqual(parseKey("\x1bOA"), "up"); assert.strictEqual(parseKey("\x1bOB"), "down"); assert.strictEqual(parseKey("\x1bOC"), "right"); assert.strictEqual(parseKey("\x1bOD"), "left"); assert.strictEqual(parseKey("\x1bOH"), "home"); assert.strictEqual(parseKey("\x1bOF"), "end"); }); it("should parse legacy function and modifier sequences", () => { assert.strictEqual(parseKey("\x1bOP"), "f1"); assert.strictEqual(parseKey("\x1b[24~"), "f12"); assert.strictEqual(parseKey("\x1b[E"), "clear"); assert.strictEqual(parseKey("\x1b[2^"), "ctrl+insert"); assert.strictEqual(parseKey("\x1bp"), "alt+up"); }); it("should parse double bracket pageUp", () => { assert.strictEqual(parseKey("\x1b[[5~"), "pageUp"); }); }); });