feat: port parse-keypress (semantic mapper) from Ink
Port Ink's parse-keypress.ts to map raw input event strings to semantic key objects with name, ctrl, shift, meta, and kitty protocol support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, test, expect } from "vite-plus/test";
|
||||||
|
import { parseKeypress } from "./parse-keypress.ts";
|
||||||
|
|
||||||
|
describe("parse-keypress", () => {
|
||||||
|
test("parses regular character", () => {
|
||||||
|
const key = parseKeypress("a");
|
||||||
|
expect(key.name).toBe("a");
|
||||||
|
expect(key.ctrl).toBe(false);
|
||||||
|
expect(key.shift).toBe(false);
|
||||||
|
expect(key.meta).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses ctrl+c", () => {
|
||||||
|
const key = parseKeypress("\x03");
|
||||||
|
expect(key.name).toBe("c");
|
||||||
|
expect(key.ctrl).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses arrow up", () => {
|
||||||
|
const key = parseKeypress("\x1b[A");
|
||||||
|
expect(key.name).toBe("up");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses arrow down", () => {
|
||||||
|
const key = parseKeypress("\x1b[B");
|
||||||
|
expect(key.name).toBe("down");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses meta+a", () => {
|
||||||
|
const key = parseKeypress("\x1ba");
|
||||||
|
expect(key.name).toBe("a");
|
||||||
|
expect(key.meta).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses backspace", () => {
|
||||||
|
const key = parseKeypress("\x7F");
|
||||||
|
expect(key.name).toBe("backspace");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses return", () => {
|
||||||
|
const key = parseKeypress("\r");
|
||||||
|
expect(key.name).toBe("return");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses tab", () => {
|
||||||
|
const key = parseKeypress("\t");
|
||||||
|
expect(key.name).toBe("tab");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses shift+tab", () => {
|
||||||
|
const key = parseKeypress("\x1b[Z");
|
||||||
|
expect(key.name).toBe("tab");
|
||||||
|
expect(key.shift).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses escape", () => {
|
||||||
|
const key = parseKeypress("\x1b");
|
||||||
|
expect(key.name).toBe("escape");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses F1", () => {
|
||||||
|
const key = parseKeypress("\x1bOP");
|
||||||
|
expect(key.name).toBe("f1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses home", () => {
|
||||||
|
const key = parseKeypress("\x1b[H");
|
||||||
|
expect(key.name).toBe("home");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses end", () => {
|
||||||
|
const key = parseKeypress("\x1b[F");
|
||||||
|
expect(key.name).toBe("end");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses delete", () => {
|
||||||
|
const key = parseKeypress("\x1b[3~");
|
||||||
|
expect(key.name).toBe("delete");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses page up", () => {
|
||||||
|
const key = parseKeypress("\x1b[5~");
|
||||||
|
expect(key.name).toBe("pageup");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uppercase letter sets shift", () => {
|
||||||
|
const key = parseKeypress("A");
|
||||||
|
expect(key.name).toBe("a");
|
||||||
|
expect(key.shift).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
// Ported from Ink's parse-keypress.ts
|
||||||
|
// @see https://github.com/vadimdemedes/ink
|
||||||
|
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
|
||||||
|
const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
|
||||||
|
|
||||||
|
const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
|
||||||
|
|
||||||
|
const keyName: Record<string, string> = {
|
||||||
|
/* xterm/gnome ESC O letter */
|
||||||
|
OP: "f1",
|
||||||
|
OQ: "f2",
|
||||||
|
OR: "f3",
|
||||||
|
OS: "f4",
|
||||||
|
/* vt220-style ESC [ letter (e.g. Ctrl+F1 sends ESC [ 1 ; 5 P) */
|
||||||
|
"[P": "f1",
|
||||||
|
"[Q": "f2",
|
||||||
|
"[R": "f3",
|
||||||
|
"[S": "f4",
|
||||||
|
/* xterm/rxvt ESC [ number ~ */
|
||||||
|
"[11~": "f1",
|
||||||
|
"[12~": "f2",
|
||||||
|
"[13~": "f3",
|
||||||
|
"[14~": "f4",
|
||||||
|
/* from Cygwin and used in libuv */
|
||||||
|
"[[A": "f1",
|
||||||
|
"[[B": "f2",
|
||||||
|
"[[C": "f3",
|
||||||
|
"[[D": "f4",
|
||||||
|
"[[E": "f5",
|
||||||
|
/* common */
|
||||||
|
"[15~": "f5",
|
||||||
|
"[17~": "f6",
|
||||||
|
"[18~": "f7",
|
||||||
|
"[19~": "f8",
|
||||||
|
"[20~": "f9",
|
||||||
|
"[21~": "f10",
|
||||||
|
"[23~": "f11",
|
||||||
|
"[24~": "f12",
|
||||||
|
/* xterm ESC [ letter */
|
||||||
|
"[A": "up",
|
||||||
|
"[B": "down",
|
||||||
|
"[C": "right",
|
||||||
|
"[D": "left",
|
||||||
|
"[E": "clear",
|
||||||
|
"[F": "end",
|
||||||
|
"[H": "home",
|
||||||
|
/* xterm/gnome ESC O letter */
|
||||||
|
OA: "up",
|
||||||
|
OB: "down",
|
||||||
|
OC: "right",
|
||||||
|
OD: "left",
|
||||||
|
OE: "clear",
|
||||||
|
OF: "end",
|
||||||
|
OH: "home",
|
||||||
|
/* xterm/rxvt ESC [ number ~ */
|
||||||
|
"[1~": "home",
|
||||||
|
"[2~": "insert",
|
||||||
|
"[3~": "delete",
|
||||||
|
"[4~": "end",
|
||||||
|
"[5~": "pageup",
|
||||||
|
"[6~": "pagedown",
|
||||||
|
/* putty */
|
||||||
|
"[[5~": "pageup",
|
||||||
|
"[[6~": "pagedown",
|
||||||
|
/* rxvt */
|
||||||
|
"[7~": "home",
|
||||||
|
"[8~": "end",
|
||||||
|
/* rxvt keys with modifiers */
|
||||||
|
"[a": "up",
|
||||||
|
"[b": "down",
|
||||||
|
"[c": "right",
|
||||||
|
"[d": "left",
|
||||||
|
"[e": "clear",
|
||||||
|
|
||||||
|
"[2$": "insert",
|
||||||
|
"[3$": "delete",
|
||||||
|
"[5$": "pageup",
|
||||||
|
"[6$": "pagedown",
|
||||||
|
"[7$": "home",
|
||||||
|
"[8$": "end",
|
||||||
|
|
||||||
|
Oa: "up",
|
||||||
|
Ob: "down",
|
||||||
|
Oc: "right",
|
||||||
|
Od: "left",
|
||||||
|
Oe: "clear",
|
||||||
|
|
||||||
|
"[2^": "insert",
|
||||||
|
"[3^": "delete",
|
||||||
|
"[5^": "pageup",
|
||||||
|
"[6^": "pagedown",
|
||||||
|
"[7^": "home",
|
||||||
|
"[8^": "end",
|
||||||
|
/* misc. */
|
||||||
|
"[Z": "tab",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nonAlphanumericKeys = [...Object.values(keyName), "backspace"];
|
||||||
|
|
||||||
|
const isShiftKey = (code: string) => {
|
||||||
|
return ["[a", "[b", "[c", "[d", "[e", "[2$", "[3$", "[5$", "[6$", "[7$", "[8$", "[Z"].includes(
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCtrlKey = (code: string) => {
|
||||||
|
return ["Oa", "Ob", "Oc", "Od", "Oe", "[2^", "[3^", "[5^", "[6^", "[7^", "[8^"].includes(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Keypress {
|
||||||
|
name: string;
|
||||||
|
ctrl: boolean;
|
||||||
|
meta: boolean;
|
||||||
|
shift: boolean;
|
||||||
|
sequence: string;
|
||||||
|
raw: string | undefined;
|
||||||
|
code?: string;
|
||||||
|
super?: boolean;
|
||||||
|
hyper?: boolean;
|
||||||
|
capsLock?: boolean;
|
||||||
|
numLock?: boolean;
|
||||||
|
eventType?: "press" | "repeat" | "release";
|
||||||
|
isKittyProtocol?: boolean;
|
||||||
|
text?: string;
|
||||||
|
/**
|
||||||
|
* Whether this key represents printable text input.
|
||||||
|
* When false, the key is a control/function/modifier key that should not
|
||||||
|
* produce text input (e.g., arrows, function keys, capslock, media keys).
|
||||||
|
* Only set by the kitty protocol parser.
|
||||||
|
*/
|
||||||
|
isPrintable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kitty keyboard protocol modifier bits.
|
||||||
|
// The actual modifier value in CSI sequences is (modifiers - 1).
|
||||||
|
const kittyModifiers = {
|
||||||
|
shift: 1,
|
||||||
|
alt: 2,
|
||||||
|
ctrl: 4,
|
||||||
|
super: 8,
|
||||||
|
hyper: 16,
|
||||||
|
meta: 32,
|
||||||
|
capsLock: 64,
|
||||||
|
numLock: 128,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Kitty keyboard protocol: CSI codepoint ; modifiers [: eventType] [; text-as-codepoints] u
|
||||||
|
const kittyKeyRe = /^\x1b\[(\d+)(?:;(\d+)(?::(\d+))?(?:;([\d:]+))?)?u$/;
|
||||||
|
|
||||||
|
// Kitty-enhanced special keys: CSI number ; modifiers : eventType {letter|~}
|
||||||
|
// These are legacy CSI sequences enhanced with the :eventType field.
|
||||||
|
// Examples: \x1b[1;1:1A (up arrow press), \x1b[3;1:3~ (delete release)
|
||||||
|
const kittySpecialKeyRe = /^\x1b\[(\d+);(\d+):(\d+)([A-Za-z~])$/;
|
||||||
|
|
||||||
|
// Letter-terminated special key names (CSI 1 ; mods letter)
|
||||||
|
const kittySpecialLetterKeys: Record<string, string> = {
|
||||||
|
A: "up",
|
||||||
|
B: "down",
|
||||||
|
C: "right",
|
||||||
|
D: "left",
|
||||||
|
E: "clear",
|
||||||
|
F: "end",
|
||||||
|
H: "home",
|
||||||
|
P: "f1",
|
||||||
|
Q: "f2",
|
||||||
|
R: "f3",
|
||||||
|
S: "f4",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Number-terminated special key names (CSI number ; mods ~)
|
||||||
|
const kittySpecialNumberKeys: Record<number, string> = {
|
||||||
|
2: "insert",
|
||||||
|
3: "delete",
|
||||||
|
5: "pageup",
|
||||||
|
6: "pagedown",
|
||||||
|
7: "home",
|
||||||
|
8: "end",
|
||||||
|
11: "f1",
|
||||||
|
12: "f2",
|
||||||
|
13: "f3",
|
||||||
|
14: "f4",
|
||||||
|
15: "f5",
|
||||||
|
17: "f6",
|
||||||
|
18: "f7",
|
||||||
|
19: "f8",
|
||||||
|
20: "f9",
|
||||||
|
21: "f10",
|
||||||
|
23: "f11",
|
||||||
|
24: "f12",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map of special codepoints to key names in kitty protocol
|
||||||
|
const kittyCodepointNames: Record<number, string> = {
|
||||||
|
27: "escape",
|
||||||
|
// 13 (return) and 32 (space) are handled before this lookup
|
||||||
|
// in parseKittyKeypress so they can be marked as printable.
|
||||||
|
9: "tab",
|
||||||
|
127: "backspace",
|
||||||
|
8: "backspace",
|
||||||
|
57358: "capslock",
|
||||||
|
57359: "scrolllock",
|
||||||
|
57360: "numlock",
|
||||||
|
57361: "printscreen",
|
||||||
|
57362: "pause",
|
||||||
|
57363: "menu",
|
||||||
|
57376: "f13",
|
||||||
|
57377: "f14",
|
||||||
|
57378: "f15",
|
||||||
|
57379: "f16",
|
||||||
|
57380: "f17",
|
||||||
|
57381: "f18",
|
||||||
|
57382: "f19",
|
||||||
|
57383: "f20",
|
||||||
|
57384: "f21",
|
||||||
|
57385: "f22",
|
||||||
|
57386: "f23",
|
||||||
|
57387: "f24",
|
||||||
|
57388: "f25",
|
||||||
|
57389: "f26",
|
||||||
|
57390: "f27",
|
||||||
|
57391: "f28",
|
||||||
|
57392: "f29",
|
||||||
|
57393: "f30",
|
||||||
|
57394: "f31",
|
||||||
|
57395: "f32",
|
||||||
|
57396: "f33",
|
||||||
|
57397: "f34",
|
||||||
|
57398: "f35",
|
||||||
|
57399: "kp0",
|
||||||
|
57400: "kp1",
|
||||||
|
57401: "kp2",
|
||||||
|
57402: "kp3",
|
||||||
|
57403: "kp4",
|
||||||
|
57404: "kp5",
|
||||||
|
57405: "kp6",
|
||||||
|
57406: "kp7",
|
||||||
|
57407: "kp8",
|
||||||
|
57408: "kp9",
|
||||||
|
57409: "kpdecimal",
|
||||||
|
57410: "kpdivide",
|
||||||
|
57411: "kpmultiply",
|
||||||
|
57412: "kpsubtract",
|
||||||
|
57413: "kpadd",
|
||||||
|
57414: "kpenter",
|
||||||
|
57415: "kpequal",
|
||||||
|
57416: "kpseparator",
|
||||||
|
57417: "kpleft",
|
||||||
|
57418: "kpright",
|
||||||
|
57419: "kpup",
|
||||||
|
57420: "kpdown",
|
||||||
|
57421: "kppageup",
|
||||||
|
57422: "kppagedown",
|
||||||
|
57423: "kphome",
|
||||||
|
57424: "kpend",
|
||||||
|
57425: "kpinsert",
|
||||||
|
57426: "kpdelete",
|
||||||
|
57427: "kpbegin",
|
||||||
|
57428: "mediaplay",
|
||||||
|
57429: "mediapause",
|
||||||
|
57430: "mediaplaypause",
|
||||||
|
57431: "mediareverse",
|
||||||
|
57432: "mediastop",
|
||||||
|
57433: "mediafastforward",
|
||||||
|
57434: "mediarewind",
|
||||||
|
57435: "mediatracknext",
|
||||||
|
57436: "mediatrackprevious",
|
||||||
|
57437: "mediarecord",
|
||||||
|
57438: "lowervolume",
|
||||||
|
57439: "raisevolume",
|
||||||
|
57440: "mutevolume",
|
||||||
|
57441: "leftshift",
|
||||||
|
57442: "leftcontrol",
|
||||||
|
57443: "leftalt",
|
||||||
|
57444: "leftsuper",
|
||||||
|
57445: "lefthyper",
|
||||||
|
57446: "leftmeta",
|
||||||
|
57447: "rightshift",
|
||||||
|
57448: "rightcontrol",
|
||||||
|
57449: "rightalt",
|
||||||
|
57450: "rightsuper",
|
||||||
|
57451: "righthyper",
|
||||||
|
57452: "rightmeta",
|
||||||
|
57453: "isoLevel3Shift",
|
||||||
|
57454: "isoLevel5Shift",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Valid Unicode codepoint range, excluding surrogates
|
||||||
|
const isValidCodepoint = (cp: number): boolean =>
|
||||||
|
cp >= 0 && cp <= 0x10_ffff && !(cp >= 0xd8_00 && cp <= 0xdf_ff);
|
||||||
|
|
||||||
|
const safeFromCodePoint = (cp: number): string =>
|
||||||
|
isValidCodepoint(cp) ? String.fromCodePoint(cp) : "?";
|
||||||
|
|
||||||
|
type EventType = "press" | "repeat" | "release";
|
||||||
|
|
||||||
|
function resolveEventType(value: number): EventType {
|
||||||
|
if (value === 3) return "release";
|
||||||
|
if (value === 2) return "repeat";
|
||||||
|
return "press";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKittyModifiers(
|
||||||
|
modifiers: number,
|
||||||
|
): Pick<Keypress, "ctrl" | "shift" | "meta" | "super" | "hyper" | "capsLock" | "numLock"> {
|
||||||
|
return {
|
||||||
|
ctrl: !!(modifiers & kittyModifiers.ctrl),
|
||||||
|
shift: !!(modifiers & kittyModifiers.shift),
|
||||||
|
meta: !!(modifiers & (kittyModifiers.meta | kittyModifiers.alt)),
|
||||||
|
super: !!(modifiers & kittyModifiers.super),
|
||||||
|
hyper: !!(modifiers & kittyModifiers.hyper),
|
||||||
|
capsLock: !!(modifiers & kittyModifiers.capsLock),
|
||||||
|
numLock: !!(modifiers & kittyModifiers.numLock),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseKittyKeypress = (s: string): Keypress | null => {
|
||||||
|
const match = kittyKeyRe.exec(s);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const codepoint = parseInt(match[1]!, 10);
|
||||||
|
const modifiers = match[2] ? Math.max(0, parseInt(match[2], 10) - 1) : 0;
|
||||||
|
const eventType = match[3] ? parseInt(match[3], 10) : 1;
|
||||||
|
const textField = match[4];
|
||||||
|
|
||||||
|
// Bail on invalid primary codepoint
|
||||||
|
if (!isValidCodepoint(codepoint)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse text-as-codepoints field (colon-separated Unicode codepoints)
|
||||||
|
let text: string | undefined;
|
||||||
|
if (textField) {
|
||||||
|
text = textField
|
||||||
|
.split(":")
|
||||||
|
.map((cp) => safeFromCodePoint(parseInt(cp, 10)))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine key name from codepoint
|
||||||
|
let name: string;
|
||||||
|
let isPrintable: boolean;
|
||||||
|
if (codepoint === 32) {
|
||||||
|
name = "space";
|
||||||
|
isPrintable = true;
|
||||||
|
} else if (codepoint === 13) {
|
||||||
|
name = "return";
|
||||||
|
isPrintable = true;
|
||||||
|
} else if (kittyCodepointNames[codepoint]) {
|
||||||
|
name = kittyCodepointNames[codepoint]!;
|
||||||
|
isPrintable = false;
|
||||||
|
} else if (codepoint >= 1 && codepoint <= 26) {
|
||||||
|
// Ctrl+letter comes as codepoint 1-26
|
||||||
|
name = String.fromCodePoint(codepoint + 96); // 'a' is 97
|
||||||
|
isPrintable = false;
|
||||||
|
} else {
|
||||||
|
name = safeFromCodePoint(codepoint).toLowerCase();
|
||||||
|
isPrintable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default text to the character from the codepoint when not explicitly
|
||||||
|
// provided by the protocol, so keys like space and return produce their
|
||||||
|
// expected text input (' ' and '\r' respectively).
|
||||||
|
if (isPrintable && !text) {
|
||||||
|
text = safeFromCodePoint(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
...parseKittyModifiers(modifiers),
|
||||||
|
eventType: resolveEventType(eventType),
|
||||||
|
sequence: s,
|
||||||
|
raw: s,
|
||||||
|
isKittyProtocol: true,
|
||||||
|
isPrintable,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse kitty-enhanced special key sequences (arrow keys, function keys, etc.)
|
||||||
|
// These use the legacy CSI format but with an added :eventType field.
|
||||||
|
const parseKittySpecialKey = (s: string): Keypress | null => {
|
||||||
|
const match = kittySpecialKeyRe.exec(s);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const number = parseInt(match[1]!, 10);
|
||||||
|
const modifiers = Math.max(0, parseInt(match[2]!, 10) - 1);
|
||||||
|
const eventType = parseInt(match[3]!, 10);
|
||||||
|
const terminator = match[4]!;
|
||||||
|
|
||||||
|
const name =
|
||||||
|
terminator === "~" ? kittySpecialNumberKeys[number] : kittySpecialLetterKeys[terminator];
|
||||||
|
|
||||||
|
if (!name) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
...parseKittyModifiers(modifiers),
|
||||||
|
eventType: resolveEventType(eventType),
|
||||||
|
sequence: s,
|
||||||
|
raw: s,
|
||||||
|
isKittyProtocol: true,
|
||||||
|
isPrintable: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseKeypress(s: Uint8Array | string = ""): Keypress {
|
||||||
|
let parts;
|
||||||
|
|
||||||
|
if (s instanceof Uint8Array) {
|
||||||
|
if (s[0]! > 127 && s[1] === undefined) {
|
||||||
|
(s[0] as unknown as number) -= 128;
|
||||||
|
s = "\x1b" + textDecoder.decode(s);
|
||||||
|
} else {
|
||||||
|
s = textDecoder.decode(s);
|
||||||
|
}
|
||||||
|
} else if (s !== undefined && typeof s !== "string") {
|
||||||
|
s = String(s);
|
||||||
|
} else if (!s) {
|
||||||
|
s = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try kitty keyboard protocol parsers first
|
||||||
|
const kittyResult = parseKittyKeypress(s);
|
||||||
|
if (kittyResult) return kittyResult;
|
||||||
|
|
||||||
|
const kittySpecialResult = parseKittySpecialKey(s);
|
||||||
|
if (kittySpecialResult) return kittySpecialResult;
|
||||||
|
|
||||||
|
// If the input matched the kitty CSI-u pattern but was rejected (e.g.,
|
||||||
|
// invalid codepoint), return a safe empty keypress instead of falling
|
||||||
|
// through to legacy parsing which can produce unsafe states (undefined name)
|
||||||
|
if (kittyKeyRe.test(s)) {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
sequence: s,
|
||||||
|
raw: s,
|
||||||
|
isKittyProtocol: true,
|
||||||
|
isPrintable: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const key: Keypress = {
|
||||||
|
name: "",
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
sequence: s,
|
||||||
|
raw: s,
|
||||||
|
};
|
||||||
|
|
||||||
|
key.sequence = key.sequence || s || key.name;
|
||||||
|
|
||||||
|
if (s === "\r" || s === "\x1b\r") {
|
||||||
|
// carriage return (or meta+return on macOS)
|
||||||
|
key.raw = undefined;
|
||||||
|
key.name = "return";
|
||||||
|
key.meta = s.length === 2;
|
||||||
|
} else if (s === "\n") {
|
||||||
|
// enter, should have been called linefeed
|
||||||
|
key.name = "enter";
|
||||||
|
} else if (s === "\t") {
|
||||||
|
// tab
|
||||||
|
key.name = "tab";
|
||||||
|
} else if (s === "\b" || s === "\x1b\b") {
|
||||||
|
// backspace or ctrl+h
|
||||||
|
key.name = "backspace";
|
||||||
|
key.meta = s.charAt(0) === "\x1b";
|
||||||
|
} else if (s === "\x7f" || s === "\x1b\x7f") {
|
||||||
|
// backspace
|
||||||
|
key.name = "backspace";
|
||||||
|
key.meta = s.charAt(0) === "\x1b";
|
||||||
|
} else if (s === "\x1b" || s === "\x1b\x1b") {
|
||||||
|
// escape key
|
||||||
|
key.name = "escape";
|
||||||
|
key.meta = s.length === 2;
|
||||||
|
} else if (s === " " || s === "\x1b ") {
|
||||||
|
key.name = "space";
|
||||||
|
key.meta = s.length === 2;
|
||||||
|
} else if (s.length === 1 && s <= "\x1a") {
|
||||||
|
// ctrl+letter
|
||||||
|
key.name = String.fromCharCode(s.charCodeAt(0) + "a".charCodeAt(0) - 1);
|
||||||
|
key.ctrl = true;
|
||||||
|
} else if (s.length === 1 && s >= "0" && s <= "9") {
|
||||||
|
// number
|
||||||
|
key.name = "number";
|
||||||
|
} else if (s.length === 1 && s >= "a" && s <= "z") {
|
||||||
|
// lowercase letter
|
||||||
|
key.name = s;
|
||||||
|
} else if (s.length === 1 && s >= "A" && s <= "Z") {
|
||||||
|
// shift+letter
|
||||||
|
key.name = s.toLowerCase();
|
||||||
|
key.shift = true;
|
||||||
|
} else if ((parts = metaKeyCodeRe.exec(s))) {
|
||||||
|
// meta+character key
|
||||||
|
key.name = parts[1]!.toLowerCase();
|
||||||
|
key.meta = true;
|
||||||
|
key.shift = /^[A-Z]$/.test(parts[1]!);
|
||||||
|
} else if ((parts = fnKeyRe.exec(s))) {
|
||||||
|
const segs = [...s];
|
||||||
|
|
||||||
|
if (segs[0] === "" && segs[1] === "") {
|
||||||
|
key.meta = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ansi escape sequence
|
||||||
|
// reassemble the key code leaving out leading \x1b's,
|
||||||
|
// the modifier key bitflag and any meaningless "1;" sequence
|
||||||
|
const code = [parts[1], parts[2], parts[4], parts[6]].filter(Boolean).join("");
|
||||||
|
|
||||||
|
const modifier = ((parts[3] || parts[5] || 1) as number) - 1;
|
||||||
|
|
||||||
|
// Parse the key modifier
|
||||||
|
key.ctrl = !!(modifier & 4);
|
||||||
|
key.meta = key.meta || !!(modifier & 10);
|
||||||
|
key.shift = !!(modifier & 1);
|
||||||
|
key.code = code;
|
||||||
|
|
||||||
|
key.name = keyName[code] ?? "";
|
||||||
|
key.shift = isShiftKey(code) || key.shift;
|
||||||
|
key.ctrl = isCtrlKey(code) || key.ctrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user