diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index c6bfee55..4c8093a5 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -3,6 +3,7 @@ * * Supports both legacy terminal sequences and Kitty keyboard protocol. * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ + * Reference: https://github.com/sst/opentui/blob/7da92b4088aebfe27b9f691c04163a48821e49fd/packages/core/src/lib/parse.keypress.ts * * Symbol keys are also supported, however some ctrl+symbol combos * overlap with ASCII codes, e.g. ctrl+[ = ESC. @@ -112,6 +113,8 @@ type SpecialKey = | "space" | "backspace" | "delete" + | "insert" + | "clear" | "home" | "end" | "pageUp" @@ -119,7 +122,19 @@ type SpecialKey = | "up" | "down" | "left" - | "right"; + | "right" + | "f1" + | "f2" + | "f3" + | "f4" + | "f5" + | "f6" + | "f7" + | "f8" + | "f9" + | "f10" + | "f11" + | "f12"; type BaseKey = Letter | SymbolKey | SpecialKey; @@ -164,6 +179,8 @@ export const Key = { space: "space" as const, backspace: "backspace" as const, delete: "delete" as const, + insert: "insert" as const, + clear: "clear" as const, home: "home" as const, end: "end" as const, pageUp: "pageUp" as const, @@ -172,6 +189,18 @@ export const Key = { down: "down" as const, left: "left" as const, right: "right" as const, + f1: "f1" as const, + f2: "f2" as const, + f3: "f3" as const, + f4: "f4" as const, + f5: "f5" as const, + f6: "f6" as const, + f7: "f7" as const, + f8: "f8" as const, + f9: "f9" as const, + f10: "f10" as const, + f11: "f11" as const, + f12: "f12" as const, // Symbol keys backtick: "`" as const, @@ -294,6 +323,135 @@ const FUNCTIONAL_CODEPOINTS = { end: -15, } as const; +const LEGACY_KEY_SEQUENCES = { + up: ["\x1b[A", "\x1bOA"], + down: ["\x1b[B", "\x1bOB"], + right: ["\x1b[C", "\x1bOC"], + left: ["\x1b[D", "\x1bOD"], + home: ["\x1b[H", "\x1bOH", "\x1b[1~", "\x1b[7~"], + end: ["\x1b[F", "\x1bOF", "\x1b[4~", "\x1b[8~"], + insert: ["\x1b[2~"], + delete: ["\x1b[3~"], + pageUp: ["\x1b[5~", "\x1b[[5~"], + pageDown: ["\x1b[6~", "\x1b[[6~"], + clear: ["\x1b[E", "\x1bOE"], + f1: ["\x1bOP", "\x1b[11~", "\x1b[[A"], + f2: ["\x1bOQ", "\x1b[12~", "\x1b[[B"], + f3: ["\x1bOR", "\x1b[13~", "\x1b[[C"], + f4: ["\x1bOS", "\x1b[14~", "\x1b[[D"], + f5: ["\x1b[15~", "\x1b[[E"], + f6: ["\x1b[17~"], + f7: ["\x1b[18~"], + f8: ["\x1b[19~"], + f9: ["\x1b[20~"], + f10: ["\x1b[21~"], + f11: ["\x1b[23~"], + f12: ["\x1b[24~"], +} as const; + +const LEGACY_SHIFT_SEQUENCES = { + up: ["\x1b[a"], + down: ["\x1b[b"], + right: ["\x1b[c"], + left: ["\x1b[d"], + clear: ["\x1b[e"], + insert: ["\x1b[2$"], + delete: ["\x1b[3$"], + pageUp: ["\x1b[5$"], + pageDown: ["\x1b[6$"], + home: ["\x1b[7$"], + end: ["\x1b[8$"], +} as const; + +const LEGACY_CTRL_SEQUENCES = { + up: ["\x1bOa"], + down: ["\x1bOb"], + right: ["\x1bOc"], + left: ["\x1bOd"], + clear: ["\x1bOe"], + insert: ["\x1b[2^"], + delete: ["\x1b[3^"], + pageUp: ["\x1b[5^"], + pageDown: ["\x1b[6^"], + home: ["\x1b[7^"], + end: ["\x1b[8^"], +} as const; + +const LEGACY_SEQUENCE_KEY_IDS: Record = { + "\x1bOA": "up", + "\x1bOB": "down", + "\x1bOC": "right", + "\x1bOD": "left", + "\x1bOH": "home", + "\x1bOF": "end", + "\x1b[E": "clear", + "\x1bOE": "clear", + "\x1bOe": "ctrl+clear", + "\x1b[e": "shift+clear", + "\x1b[2~": "insert", + "\x1b[2$": "shift+insert", + "\x1b[2^": "ctrl+insert", + "\x1b[3$": "shift+delete", + "\x1b[3^": "ctrl+delete", + "\x1b[[5~": "pageUp", + "\x1b[[6~": "pageDown", + "\x1b[a": "shift+up", + "\x1b[b": "shift+down", + "\x1b[c": "shift+right", + "\x1b[d": "shift+left", + "\x1bOa": "ctrl+up", + "\x1bOb": "ctrl+down", + "\x1bOc": "ctrl+right", + "\x1bOd": "ctrl+left", + "\x1b[5$": "shift+pageUp", + "\x1b[6$": "shift+pageDown", + "\x1b[7$": "shift+home", + "\x1b[8$": "shift+end", + "\x1b[5^": "ctrl+pageUp", + "\x1b[6^": "ctrl+pageDown", + "\x1b[7^": "ctrl+home", + "\x1b[8^": "ctrl+end", + "\x1bOP": "f1", + "\x1bOQ": "f2", + "\x1bOR": "f3", + "\x1bOS": "f4", + "\x1b[11~": "f1", + "\x1b[12~": "f2", + "\x1b[13~": "f3", + "\x1b[14~": "f4", + "\x1b[[A": "f1", + "\x1b[[B": "f2", + "\x1b[[C": "f3", + "\x1b[[D": "f4", + "\x1b[[E": "f5", + "\x1b[15~": "f5", + "\x1b[17~": "f6", + "\x1b[18~": "f7", + "\x1b[19~": "f8", + "\x1b[20~": "f9", + "\x1b[21~": "f10", + "\x1b[23~": "f11", + "\x1b[24~": "f12", + "\x1bb": "alt+left", + "\x1bf": "alt+right", + "\x1bp": "alt+up", + "\x1bn": "alt+down", +} as const; + +type LegacyModifierKey = keyof typeof LEGACY_SHIFT_SEQUENCES; + +const matchesLegacySequence = (data: string, sequences: readonly string[]): boolean => sequences.includes(data); + +const matchesLegacyModifierSequence = (data: string, key: LegacyModifierKey, modifier: number): boolean => { + if (modifier === MODIFIERS.shift) { + return matchesLegacySequence(data, LEGACY_SHIFT_SEQUENCES[key]); + } + if (modifier === MODIFIERS.ctrl) { + return matchesLegacySequence(data, LEGACY_CTRL_SEQUENCES[key]); + } + return false; +}; + // ============================================================================= // Kitty Protocol Parsing // ============================================================================= @@ -534,6 +692,14 @@ export function matchesKey(data: string, keyId: KeyId): boolean { return data === "\x1b" || matchesKittySequence(data, CODEPOINTS.escape, 0); case "space": + if (!_kittyProtocolActive) { + if (ctrl && !alt && !shift && data === "\x00") { + return true; + } + if (alt && !ctrl && !shift && data === "\x1b ") { + return true; + } + } if (modifier === 0) { return data === " " || matchesKittySequence(data, CODEPOINTS.space, 0); } @@ -592,6 +758,7 @@ export function matchesKey(data: string, keyId: KeyId): boolean { if (modifier === 0) { return ( data === "\r" || + (!_kittyProtocolActive && data === "\n") || data === "\x1bOM" || // SS3 M (numpad enter in some terminals) matchesKittySequence(data, CODEPOINTS.enter, 0) || matchesKittySequence(data, CODEPOINTS.kpEnter, 0) @@ -604,62 +771,121 @@ export function matchesKey(data: string, keyId: KeyId): boolean { case "backspace": if (alt && !ctrl && !shift) { - return data === "\x1b\x7f" || matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt); + if (!_kittyProtocolActive) { + return data === "\x1b\x7f" || data === "\x1b\b"; + } + return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt); } if (modifier === 0) { return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0); } return matchesKittySequence(data, CODEPOINTS.backspace, modifier); + case "insert": + if (modifier === 0) { + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.insert) || + matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.insert, 0) + ); + } + if (matchesLegacyModifierSequence(data, "insert", modifier)) { + return true; + } + return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.insert, modifier); + case "delete": if (modifier === 0) { - return data === "\x1b[3~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.delete) || + matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0) + ); + } + if (matchesLegacyModifierSequence(data, "delete", modifier)) { + return true; } return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, modifier); + case "clear": + if (modifier === 0) { + return matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.clear); + } + return matchesLegacyModifierSequence(data, "clear", modifier); + case "home": if (modifier === 0) { return ( - data === "\x1b[H" || - data === "\x1b[1~" || - data === "\x1b[7~" || + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.home) || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, 0) ); } + if (matchesLegacyModifierSequence(data, "home", modifier)) { + return true; + } return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, modifier); case "end": if (modifier === 0) { return ( - data === "\x1b[F" || - data === "\x1b[4~" || - data === "\x1b[8~" || + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.end) || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, 0) ); } + if (matchesLegacyModifierSequence(data, "end", modifier)) { + return true; + } return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, modifier); case "pageUp": if (modifier === 0) { - return data === "\x1b[5~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageUp, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.pageUp) || + matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageUp, 0) + ); + } + if (matchesLegacyModifierSequence(data, "pageUp", modifier)) { + return true; } return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageUp, modifier); case "pageDown": if (modifier === 0) { - return data === "\x1b[6~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageDown, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.pageDown) || + matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageDown, 0) + ); + } + if (matchesLegacyModifierSequence(data, "pageDown", modifier)) { + return true; } return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageDown, modifier); case "up": + if (alt && !ctrl && !shift) { + return data === "\x1bp" || matchesKittySequence(data, ARROW_CODEPOINTS.up, MODIFIERS.alt); + } if (modifier === 0) { - return data === "\x1b[A" || matchesKittySequence(data, ARROW_CODEPOINTS.up, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.up) || + matchesKittySequence(data, ARROW_CODEPOINTS.up, 0) + ); + } + if (matchesLegacyModifierSequence(data, "up", modifier)) { + return true; } return matchesKittySequence(data, ARROW_CODEPOINTS.up, modifier); case "down": + if (alt && !ctrl && !shift) { + return data === "\x1bn" || matchesKittySequence(data, ARROW_CODEPOINTS.down, MODIFIERS.alt); + } if (modifier === 0) { - return data === "\x1b[B" || matchesKittySequence(data, ARROW_CODEPOINTS.down, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.down) || + matchesKittySequence(data, ARROW_CODEPOINTS.down, 0) + ); + } + if (matchesLegacyModifierSequence(data, "down", modifier)) { + return true; } return matchesKittySequence(data, ARROW_CODEPOINTS.down, modifier); @@ -667,15 +893,26 @@ export function matchesKey(data: string, keyId: KeyId): boolean { if (alt && !ctrl && !shift) { return ( data === "\x1b[1;3D" || + (!_kittyProtocolActive && data === "\x1bB") || data === "\x1bb" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt) ); } if (ctrl && !alt && !shift) { - return data === "\x1b[1;5D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl); + return ( + data === "\x1b[1;5D" || + matchesLegacyModifierSequence(data, "left", MODIFIERS.ctrl) || + matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl) + ); } if (modifier === 0) { - return data === "\x1b[D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.left) || + matchesKittySequence(data, ARROW_CODEPOINTS.left, 0) + ); + } + if (matchesLegacyModifierSequence(data, "left", modifier)) { + return true; } return matchesKittySequence(data, ARROW_CODEPOINTS.left, modifier); @@ -683,23 +920,57 @@ export function matchesKey(data: string, keyId: KeyId): boolean { if (alt && !ctrl && !shift) { return ( data === "\x1b[1;3C" || + (!_kittyProtocolActive && data === "\x1bF") || data === "\x1bf" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt) ); } if (ctrl && !alt && !shift) { - return data === "\x1b[1;5C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl); + return ( + data === "\x1b[1;5C" || + matchesLegacyModifierSequence(data, "right", MODIFIERS.ctrl) || + matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl) + ); } if (modifier === 0) { - return data === "\x1b[C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, 0); + return ( + matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.right) || + matchesKittySequence(data, ARROW_CODEPOINTS.right, 0) + ); + } + if (matchesLegacyModifierSequence(data, "right", modifier)) { + return true; } return matchesKittySequence(data, ARROW_CODEPOINTS.right, modifier); + + case "f1": + case "f2": + case "f3": + case "f4": + case "f5": + case "f6": + case "f7": + case "f8": + case "f9": + case "f10": + case "f11": + case "f12": { + if (modifier !== 0) { + return false; + } + const functionKey = key as keyof typeof LEGACY_KEY_SEQUENCES; + return matchesLegacySequence(data, LEGACY_KEY_SEQUENCES[functionKey]); + } } // Handle single letter keys (a-z) and some symbols if (key.length === 1 && ((key >= "a" && key <= "z") || SYMBOL_KEYS.has(key))) { const codepoint = key.charCodeAt(0); + if (ctrl && alt && !shift && !_kittyProtocolActive && key >= "a" && key <= "z") { + return data === `\x1b${rawCtrlChar(key)}`; + } + if (ctrl && !shift && !alt) { const raw = rawCtrlChar(key); if (data === raw) return true; @@ -755,6 +1026,7 @@ export function parseKey(data: string): string | undefined { else if (effectiveCodepoint === CODEPOINTS.space) keyName = "space"; else if (effectiveCodepoint === CODEPOINTS.backspace) keyName = "backspace"; else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.delete) keyName = "delete"; + else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.insert) keyName = "insert"; else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.home) keyName = "home"; else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.end) keyName = "end"; else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageUp) keyName = "pageUp"; @@ -780,21 +1052,34 @@ export function parseKey(data: string): string | undefined { if (data === "\x1b\r" || data === "\n") return "shift+enter"; } + const legacySequenceKeyId = LEGACY_SEQUENCE_KEY_IDS[data]; + if (legacySequenceKeyId) return legacySequenceKeyId; + // Legacy sequences (used when Kitty protocol is not active, or for unambiguous sequences) if (data === "\x1b") return "escape"; if (data === "\t") return "tab"; - if (data === "\r" || data === "\x1bOM") return "enter"; + if (data === "\r" || (!_kittyProtocolActive && data === "\n") || data === "\x1bOM") return "enter"; + if (data === "\x00") return "ctrl+space"; if (data === " ") return "space"; if (data === "\x7f" || data === "\x08") return "backspace"; if (data === "\x1b[Z") return "shift+tab"; if (!_kittyProtocolActive && data === "\x1b\r") return "alt+enter"; - if (data === "\x1b\x7f") return "alt+backspace"; + if (!_kittyProtocolActive && data === "\x1b ") return "alt+space"; + if (!_kittyProtocolActive && (data === "\x1b\x7f" || data === "\x1b\b")) return "alt+backspace"; + if (!_kittyProtocolActive && data === "\x1bB") return "alt+left"; + if (!_kittyProtocolActive && data === "\x1bF") return "alt+right"; + if (!_kittyProtocolActive && data.length === 2 && data[0] === "\x1b") { + const code = data.charCodeAt(1); + if (code >= 1 && code <= 26) { + return `ctrl+alt+${String.fromCharCode(code + 96)}`; + } + } if (data === "\x1b[A") return "up"; if (data === "\x1b[B") return "down"; if (data === "\x1b[C") return "right"; if (data === "\x1b[D") return "left"; - if (data === "\x1b[H") return "home"; - if (data === "\x1b[F") return "end"; + if (data === "\x1b[H" || data === "\x1bOH") return "home"; + if (data === "\x1b[F" || data === "\x1bOF") return "end"; if (data === "\x1b[3~") return "delete"; if (data === "\x1b[5~") return "pageUp"; if (data === "\x1b[6~") return "pageDown"; diff --git a/packages/tui/test/keys.test.ts b/packages/tui/test/keys.test.ts index af153126..3f936aad 100644 --- a/packages/tui/test/keys.test.ts +++ b/packages/tui/test/keys.test.ts @@ -117,12 +117,87 @@ describe("matchesKey", () => { 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 parse legacy alt-prefixed sequences only 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"); + + setKittyProtocolActive(true); + assert.strictEqual(matchesKey("\x1b ", "alt+space"), false); + assert.strictEqual(parseKey("\x1b "), undefined); + assert.strictEqual(matchesKey("\x1b\b", "alt+backspace"), false); + assert.strictEqual(parseKey("\x1b\b"), undefined); + 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); + 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); + }); }); }); @@ -155,6 +230,8 @@ describe("parseKey", () => { 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"); }); @@ -164,5 +241,26 @@ describe("parseKey", () => { 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"); + }); }); });