Expand keymap coverage and add key tests (#752)

This commit is contained in:
Richard Gill 2026-01-15 23:21:37 +00:00 committed by GitHub
parent cceb5908d9
commit 131f1a25f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 404 additions and 21 deletions

View file

@ -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<string, KeyId> = {
"\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";

View file

@ -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");
});
});
});