From 92b123a7fa8ed948c04dd3174eaff33ad44e905a Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 27 Mar 2026 23:44:04 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix=20keycode=20mapping=20and=20?= =?UTF-8?q?legacy=20CSI=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct modifier key codepoint mapping to match the spec (all-lefts-first 57441–57446, then all-rights 57447–57452). Add parser for Kitty-enhanced legacy CSI sequences (arrows, function keys, tilde-style) that include `:action` sub-parameters. --- .gitignore | 1 + demo/keyboard.ts | 126 ++++++++++++++---------------- input-native.ts | 34 ++++++++ input.ts | 102 ++++++++++++++++++++++-- src/input.c | 190 +++++++++++++++++++++++++++++++++++++++++++++ src/input.h | 36 +++++++++ test/input.test.ts | 69 +++++++++++++++- 7 files changed, 484 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index d159309..83e7faa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.agent-shell/ /clayterm.wasm /build/ +/node_modules/ diff --git a/demo/keyboard.ts b/demo/keyboard.ts index e06b61b..82eac0a 100644 --- a/demo/keyboard.ts +++ b/demo/keyboard.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file no-fallthrough import { each, ensure, main, until } from "effection"; import { close, @@ -34,18 +35,11 @@ function isKeyEvent(e: InputEvent): e is KeyEvent { return e.type === "keydown" || e.type === "keyrepeat" || e.type === "keyup"; } -function is(char: string): (event: InputEvent) => boolean { +function is(...codes: string[]): (event: InputEvent) => boolean { return (e) => - isKeyEvent(e) && e.key.toUpperCase() === char.toUpperCase(); + isKeyEvent(e) && e.type === "keydown" && codes.some((c) => e.code.toUpperCase() === c.toUpperCase()); } -function mod(name: "ctrl" | "alt" | "shift"): (event: InputEvent) => boolean { - return (e) => isKeyEvent(e) && e[name] === true; -} - -function never(): boolean { - return false; -} function key(ops: Op[], k: KeyDef, ctx: AppContext): void { let bg = ctx.event && k.match(ctx.event) ? active : inactive; @@ -139,7 +133,7 @@ function mainKeys(ops: Op[], ctx: AppContext): void { ], ctx); row(ops, [ - { label: "Caps", width: 9, match: never }, + { label: "Caps", width: 9, match: is("CapsLock") }, { label: "A", match: is("a") }, { label: "S", match: is("s") }, { label: "D", match: is("d") }, @@ -155,7 +149,7 @@ function mainKeys(ops: Op[], ctx: AppContext): void { ], ctx); row(ops, [ - { label: "Shift", width: 11, match: mod("shift") }, + { label: "Shift", width: 11, match: is("ShiftLeft") }, { label: "Z", match: is("z") }, { label: "X", match: is("x") }, { label: "C", match: is("c") }, @@ -166,18 +160,18 @@ function mainKeys(ops: Op[], ctx: AppContext): void { { label: ",", match: is(",") }, { label: ".", match: is(".") }, { label: "/", match: is("/") }, - { label: "Shift", width: 13, match: mod("shift") }, + { label: "Shift", width: 13, match: is("ShiftRight") }, ], ctx); row(ops, [ - { label: "Ctrl", width: 7, match: mod("ctrl") }, - { label: "Win", width: 6, match: never }, - { label: "Alt", width: 6, match: mod("alt") }, + { label: "Ctrl", width: 7, match: is("ControlLeft") }, + { label: "Win", width: 6, match: is("SuperLeft") }, + { label: "Alt", width: 6, match: is("AltLeft") }, { label: "", width: 33, match: is(" ") }, - { label: "Alt", width: 6, match: mod("alt") }, - { label: "Win", width: 6, match: never }, - { label: "Menu", width: 6, match: never }, - { label: "Ctrl", width: 7, match: mod("ctrl") }, + { label: "Alt", width: 6, match: is("AltRight") }, + { label: "Win", width: 6, match: is("SuperRight") }, + { label: "Menu", width: 6, match: is() }, + { label: "Ctrl", width: 7, match: is("ControlRight") }, ], ctx); ops.push(close()); @@ -232,10 +226,10 @@ function numpad(ops: Op[], ctx: AppContext): void { ); row(ops, [ - { label: "Num", width: 6, match: never }, - { label: "/", width: 6, match: never }, - { label: "*", width: 6, match: never }, - { label: "-", width: 6, match: never }, + { label: "Num", width: 6, match: is("NumLock") }, + { label: "/", width: 6, match: is("NumpadDivide") }, + { label: "*", width: 6, match: is("NumpadMultiply") }, + { label: "-", width: 6, match: is("NumpadSubtract") }, ], ctx); // rows 2-3 grouped horizontally so + spans both @@ -248,14 +242,14 @@ function numpad(ops: Op[], ctx: AppContext): void { open("box", { layout: { direction: "ttb", gap: GAP } }), ); row(ops, [ - { label: "7", width: 6, match: never }, - { label: "8", width: 6, match: never }, - { label: "9", width: 6, match: never }, + { label: "7", width: 6, match: is("Numpad7") }, + { label: "8", width: 6, match: is("Numpad8") }, + { label: "9", width: 6, match: is("Numpad9") }, ], ctx); row(ops, [ - { label: "4", width: 6, match: never }, - { label: "5", width: 6, match: never }, - { label: "6", width: 6, match: never }, + { label: "4", width: 6, match: is("Numpad4") }, + { label: "5", width: 6, match: is("Numpad5") }, + { label: "6", width: 6, match: is("Numpad6") }, ], ctx); ops.push(close()); @@ -287,13 +281,13 @@ function numpad(ops: Op[], ctx: AppContext): void { open("box", { layout: { direction: "ttb", gap: GAP } }), ); row(ops, [ - { label: "1", width: 6, match: never }, - { label: "2", width: 6, match: never }, - { label: "3", width: 6, match: never }, + { label: "1", width: 6, match: is("Numpad1") }, + { label: "2", width: 6, match: is("Numpad2") }, + { label: "3", width: 6, match: is("Numpad3") }, ], ctx); row(ops, [ - { label: "0", width: 13, match: never }, - { label: ".", width: 6, match: never }, + { label: "0", width: 13, match: is("Numpad0") }, + { label: ".", width: 6, match: is("NumpadDecimal") }, ], ctx); ops.push(close()); @@ -357,7 +351,7 @@ function flagPanel(ops: Op[], ctx: AppContext): void { ops.push( open("box", { layout: { height: fixed(1) } }), - text("Keyboard Protocol", { color: highlight }), + text("Keyboard Protocol Level", { color: highlight }), close(), ); @@ -401,7 +395,7 @@ function keyboard(ctx: AppContext): Op[] { let badgeLabel = ctx.mode === "input" ? "input" : "config"; let badgeHint = ctx.mode === "input" ? "Ctrl+X Ctrl+X to enter config" - : "Set flags with keys [1-5], Escape to exit"; + : "Set flags with keys [1-5], Enter to save"; ops.push( open("box", { layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } } }), open("box", { layout: { padding: { left: 1, right: 1 } }, bg: rgba(60, 60, 60) }), @@ -474,7 +468,7 @@ function ttyFlags(ctx: AppContext): Uint8Array { if (ctx["Report alternate keys"]) bits |= 4; if (ctx["Report all keys as escapes"]) bits |= 8; if (ctx["Report associated text"]) bits |= 16; - return encoder.encode(`\x1b[=${bits};3u`); + return encoder.encode(`\x1b[${bits}u`); } await main(function* () { @@ -489,9 +483,9 @@ await main(function* () { let term = yield* until(createTerm({ width: columns, height: rows })); - esc("\x1b[?1049h\x1b[?25l"); + esc("\x1b[?1049h\x1b[?25l\x1b[>3u"); yield* ensure(() => { - esc("\x1b[?25h\x1b[?1049l"); + esc("\x1b[ { let current: AppContext = { mode: "input", - "Disambiguate escape codes": false, - "Report event types": false, + "Disambiguate escape codes": true, + "Report event types": true, "Report alternate keys": false, "Report all keys as escapes": false, "Report associated text": false, @@ -544,11 +538,12 @@ function* inputmode(context: AppContext): Mode { context = { ...context, event }; if (event.type === "keydown" && event.key === "x" && event.ctrl) { let next = yield context; - context = { - ...context, - event: next, - }; - if (next.type === "keydown" && next.key === "x" && next.ctrl) { + while (next.type !== "keydown") { + context = { ...context, event: next }; + next = yield context; + } + context = { ...context, event: next }; + if (next.key === "x" && next.ctrl) { return configmode({ ...context, event: null, @@ -563,31 +558,30 @@ function* configmode(context: AppContext): Mode { context = { ...context, mode: "config" }; let event = yield context; while (true) { - if (event.type === "keydown" && event.key === "Escape") { + if (event.type === "keydown" && event.key === "Enter") { return inputmode({...context, event: null }); } - if (event.type === "keydown") { + if (event.type === "keydown" && "012345".indexOf(event.key) >= 0) { + context = { ...context }; + context["Report associated text"] = false; + context["Report all keys as escapes"] = false; + context["Report alternate keys"] = false; + context["Report event types"] = false; + context["Disambiguate escape codes"] = false; switch (event.key) { - case "1": { - context = {...context, ["Disambiguate escape codes"]: !context["Disambiguate escape codes"]}; - break; - } - case "2": { - context = {...context, ["Report event types"]: !context["Report event types"]}; - break; - } - case "3": { - context = {...context, ["Report alternate keys"]: !context["Report alternate keys"]}; - break; - } - case "4": { - context = {...context, ["Report all keys as escapes"]: !context["Report all keys as escapes"]}; + case "5": + context["Report associated text"] = true; + case "4": + context["Report all keys as escapes"] = true; + case "3": + context["Report alternate keys"] = true; + case "2": + context["Report event types"] = true; + case "1": + context["Disambiguate escape codes"] = true; break; - } - case "5": { - context = {...context, ["Report associated text"]: !context["Report associated text"]}; + case "0": break; - } } } event = yield context; diff --git a/input-native.ts b/input-native.ts index b86ce07..268ec7f 100644 --- a/input-native.ts +++ b/input-native.ts @@ -31,6 +31,40 @@ export const KEY_DELETE = 0xFFEC; export const KEY_PGUP = 0xFFEB; export const KEY_PGDN = 0xFFEA; export const KEY_BACKTAB = 0xFFE9; +export const KEY_NUMPAD_0 = 0xFFE2; +export const KEY_NUMPAD_1 = 0xFFE1; +export const KEY_NUMPAD_2 = 0xFFE0; +export const KEY_NUMPAD_3 = 0xFFDF; +export const KEY_NUMPAD_4 = 0xFFDE; +export const KEY_NUMPAD_5 = 0xFFDD; +export const KEY_NUMPAD_6 = 0xFFDC; +export const KEY_NUMPAD_7 = 0xFFDB; +export const KEY_NUMPAD_8 = 0xFFDA; +export const KEY_NUMPAD_9 = 0xFFD9; +export const KEY_NUMPAD_DECIMAL = 0xFFD8; +export const KEY_NUMPAD_DIVIDE = 0xFFD7; +export const KEY_NUMPAD_MULTIPLY = 0xFFD6; +export const KEY_NUMPAD_SUBTRACT = 0xFFD5; +export const KEY_NUMPAD_ADD = 0xFFD4; +export const KEY_NUMPAD_ENTER = 0xFFD3; +export const KEY_NUMPAD_EQUAL = 0xFFD2; + +export const KEY_SHIFT_LEFT = 0xFFD1; +export const KEY_SHIFT_RIGHT = 0xFFD0; +export const KEY_CONTROL_LEFT = 0xFFCF; +export const KEY_CONTROL_RIGHT = 0xFFCE; +export const KEY_ALT_LEFT = 0xFFCD; +export const KEY_ALT_RIGHT = 0xFFCC; +export const KEY_SUPER_LEFT = 0xFFCB; +export const KEY_SUPER_RIGHT = 0xFFCA; +export const KEY_HYPER_LEFT = 0xFFC9; +export const KEY_HYPER_RIGHT = 0xFFC8; +export const KEY_META_LEFT = 0xFFC7; +export const KEY_META_RIGHT = 0xFFC6; +export const KEY_CAPS_LOCK = 0xFFC5; +export const KEY_NUM_LOCK = 0xFFC4; +export const KEY_SCROLL_LOCK = 0xFFC3; + export const KEY_MOUSE_LEFT = 0xFFE8; export const KEY_MOUSE_RIGHT = 0xFFE7; export const KEY_MOUSE_MIDDLE = 0xFFE6; diff --git a/input.ts b/input.ts index ef391c1..fa54e1c 100644 --- a/input.ts +++ b/input.ts @@ -35,6 +35,38 @@ import { KEY_F9, KEY_HOME, KEY_INSERT, + KEY_NUMPAD_0, + KEY_NUMPAD_1, + KEY_NUMPAD_2, + KEY_NUMPAD_3, + KEY_NUMPAD_4, + KEY_NUMPAD_5, + KEY_NUMPAD_6, + KEY_NUMPAD_7, + KEY_NUMPAD_8, + KEY_NUMPAD_9, + KEY_NUMPAD_ADD, + KEY_NUMPAD_DECIMAL, + KEY_NUMPAD_DIVIDE, + KEY_NUMPAD_ENTER, + KEY_NUMPAD_EQUAL, + KEY_NUMPAD_MULTIPLY, + KEY_NUMPAD_SUBTRACT, + KEY_META_LEFT, + KEY_META_RIGHT, + KEY_NUM_LOCK, + KEY_SCROLL_LOCK, + KEY_SHIFT_LEFT, + KEY_SHIFT_RIGHT, + KEY_CONTROL_LEFT, + KEY_CONTROL_RIGHT, + KEY_ALT_LEFT, + KEY_ALT_RIGHT, + KEY_SUPER_LEFT, + KEY_SUPER_RIGHT, + KEY_HYPER_LEFT, + KEY_HYPER_RIGHT, + KEY_CAPS_LOCK, KEY_MOUSE_LEFT, KEY_MOUSE_MIDDLE, KEY_MOUSE_RELEASE, @@ -64,11 +96,39 @@ export interface KeyModifiers { shift?: true; } +/** + * Physical key identity on a US PC-101 layout. + */ +export type KeyCode = + | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" + | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" + | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" + | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + | "`" | "-" | "=" | "[" | "]" | "\\" | ";" | "'" | "," | "." | "/" | " " + | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" + | "F7" | "F8" | "F9" | "F10" | "F11" | "F12" + | "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" + | "Home" | "End" | "Insert" | "Delete" + | "PageUp" | "PageDown" | "Backtab" + | "Backspace" | "Tab" | "Enter" | "Escape" + | "Numpad0" | "Numpad1" | "Numpad2" | "Numpad3" | "Numpad4" + | "Numpad5" | "Numpad6" | "Numpad7" | "Numpad8" | "Numpad9" + | "NumpadDecimal" | "NumpadDivide" | "NumpadMultiply" + | "NumpadSubtract" | "NumpadAdd" | "NumpadEnter" | "NumpadEqual" + | "ShiftLeft" | "ShiftRight" + | "ControlLeft" | "ControlRight" + | "AltLeft" | "AltRight" + | "SuperLeft" | "SuperRight" + | "HyperLeft" | "HyperRight" + | "MetaLeft" | "MetaRight" + | "CapsLock" | "NumLock" | "ScrollLock"; + /** * Shared key information present on all keyboard events. */ export interface KeyInfo extends KeyModifiers { key: string; + code: KeyCode; } /** @@ -78,7 +138,6 @@ export interface KeyInfo extends KeyModifiers { export interface KeyDown extends KeyInfo { type: "keydown"; shifted?: string; - base?: string; text?: string; } @@ -89,7 +148,6 @@ export interface KeyDown extends KeyInfo { export interface KeyRepeat extends KeyInfo { type: "keyrepeat"; shifted?: string; - base?: string; text?: string; } @@ -349,6 +407,38 @@ const KEY_NAMES = new Map([ [KEY_TAB, "Tab"], [KEY_ENTER, "Enter"], [KEY_ESC, "Escape"], + [KEY_NUMPAD_0, "Numpad0"], + [KEY_NUMPAD_1, "Numpad1"], + [KEY_NUMPAD_2, "Numpad2"], + [KEY_NUMPAD_3, "Numpad3"], + [KEY_NUMPAD_4, "Numpad4"], + [KEY_NUMPAD_5, "Numpad5"], + [KEY_NUMPAD_6, "Numpad6"], + [KEY_NUMPAD_7, "Numpad7"], + [KEY_NUMPAD_8, "Numpad8"], + [KEY_NUMPAD_9, "Numpad9"], + [KEY_NUMPAD_DECIMAL, "NumpadDecimal"], + [KEY_NUMPAD_DIVIDE, "NumpadDivide"], + [KEY_NUMPAD_MULTIPLY, "NumpadMultiply"], + [KEY_NUMPAD_SUBTRACT, "NumpadSubtract"], + [KEY_NUMPAD_ADD, "NumpadAdd"], + [KEY_NUMPAD_ENTER, "NumpadEnter"], + [KEY_NUMPAD_EQUAL, "NumpadEqual"], + [KEY_SHIFT_LEFT, "ShiftLeft"], + [KEY_SHIFT_RIGHT, "ShiftRight"], + [KEY_CONTROL_LEFT, "ControlLeft"], + [KEY_CONTROL_RIGHT, "ControlRight"], + [KEY_ALT_LEFT, "AltLeft"], + [KEY_ALT_RIGHT, "AltRight"], + [KEY_SUPER_LEFT, "SuperLeft"], + [KEY_SUPER_RIGHT, "SuperRight"], + [KEY_HYPER_LEFT, "HyperLeft"], + [KEY_HYPER_RIGHT, "HyperRight"], + [KEY_META_LEFT, "MetaLeft"], + [KEY_META_RIGHT, "MetaRight"], + [KEY_CAPS_LOCK, "CapsLock"], + [KEY_NUM_LOCK, "NumLock"], + [KEY_SCROLL_LOCK, "ScrollLock"], ]); const BUTTON_NAMES = new Map([ @@ -389,22 +479,24 @@ function textFromNative(native: NativeInputEvent): string | undefined { function mapKeyEvent(native: NativeInputEvent): KeyEvent { let key = keyName(native); + let code = (native.base > 0 + ? String.fromCodePoint(native.base) + : key) as KeyCode; let m = mods(native); let isChar = !KEY_NAMES.has(native.key); let text = textFromNative(native); if (native.action === 3) { - return { type: "keyup", key, ...m }; + return { type: "keyup", key, code, ...m }; } let type: "keydown" | "keyrepeat" = native.action === 2 ? "keyrepeat" : "keydown"; - let ev: KeyDown | KeyRepeat = { type, key, ...m }; + let ev: KeyDown | KeyRepeat = { type, key, code, ...m }; if (native.shifted > 0) ev.shifted = String.fromCodePoint(native.shifted); - if (native.base > 0) ev.base = String.fromCodePoint(native.base); if (text) { ev.text = text; } else if (isChar && type === "keydown" && native.ch > 0) { diff --git a/src/input.c b/src/input.c index c6886df..1493c05 100644 --- a/src/input.c +++ b/src/input.c @@ -280,6 +280,70 @@ static uint16_t kitty_key(int cp) { return KEY_F11; case 57387: return KEY_F12; + case 57399: + return KEY_NUMPAD_0; + case 57400: + return KEY_NUMPAD_1; + case 57401: + return KEY_NUMPAD_2; + case 57402: + return KEY_NUMPAD_3; + case 57403: + return KEY_NUMPAD_4; + case 57404: + return KEY_NUMPAD_5; + case 57405: + return KEY_NUMPAD_6; + case 57406: + return KEY_NUMPAD_7; + case 57407: + return KEY_NUMPAD_8; + case 57408: + return KEY_NUMPAD_9; + case 57409: + return KEY_NUMPAD_DECIMAL; + case 57410: + return KEY_NUMPAD_DIVIDE; + case 57411: + return KEY_NUMPAD_MULTIPLY; + case 57412: + return KEY_NUMPAD_SUBTRACT; + case 57413: + return KEY_NUMPAD_ADD; + case 57414: + return KEY_NUMPAD_ENTER; + case 57415: + return KEY_NUMPAD_EQUAL; + case 57441: + return KEY_SHIFT_LEFT; + case 57442: + return KEY_CONTROL_LEFT; + case 57443: + return KEY_ALT_LEFT; + case 57444: + return KEY_SUPER_LEFT; + case 57445: + return KEY_HYPER_LEFT; + case 57446: + return KEY_META_LEFT; + case 57447: + return KEY_SHIFT_RIGHT; + case 57448: + return KEY_CONTROL_RIGHT; + case 57449: + return KEY_ALT_RIGHT; + case 57450: + return KEY_SUPER_RIGHT; + case 57451: + return KEY_HYPER_RIGHT; + case 57452: + return KEY_META_RIGHT; + case 57358: + return KEY_CAPS_LOCK; + case 57359: + return KEY_NUM_LOCK; + case 57360: + return KEY_SCROLL_LOCK; default: return 0; } @@ -429,6 +493,116 @@ static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { return PARSE_OK; } +static uint16_t csi_legacy_key(char term, int number) { + switch (term) { + case 'A': return KEY_ARROW_UP; + case 'B': return KEY_ARROW_DOWN; + case 'C': return KEY_ARROW_RIGHT; + case 'D': return KEY_ARROW_LEFT; + case 'H': return KEY_HOME; + case 'F': return KEY_END; + case 'P': return KEY_F1; + case 'Q': return KEY_F2; + case 'S': return KEY_F4; + case '~': + switch (number) { + case 2: return KEY_INSERT; + case 3: return KEY_DELETE; + case 5: return KEY_PGUP; + case 6: return KEY_PGDN; + case 7: return KEY_HOME; + case 8: return KEY_END; + case 11: return KEY_F1; + case 12: return KEY_F2; + case 13: return KEY_F3; + case 14: return KEY_F4; + case 15: return KEY_F5; + case 17: return KEY_F6; + case 18: return KEY_F7; + case 19: return KEY_F8; + case 20: return KEY_F9; + case 21: return KEY_F10; + case 23: return KEY_F11; + case 24: return KEY_F12; + default: return 0; + } + default: return 0; + } +} + +/* Parse Kitty-enhanced legacy CSI sequences (non-u terminators). + * Format: CSI [number] [; mod[:action]] terminator + * Handles A-D, F, H, P, Q, S, ~ terminators with optional :action */ +static int parse_csi_legacy(struct InputState *st, struct InputEvent *ev) { + if (st->len < 2) + return PARSE_NEED_MORE; + if (st->buf[0] != '\x1b' || st->buf[1] != '[') + return PARSE_ERR; + if (st->len < 3) + return PARSE_NEED_MORE; + + int number = -1; + int mod = -1; + int action = -1; + int param = 0; + int sub = 0; + int i = 2; + int cur = -1; + char term = 0; + + while (i < st->len) { + char c = st->buf[i]; + if (c >= '0' && c <= '9') { + if (cur == -1) + cur = 0; + cur = cur * 10 + (c - '0'); + } else if (c == ';') { + if (param == 0) + number = cur; + else if (param == 1 && sub == 0) + mod = cur; + cur = -1; + param++; + sub = 0; + } else if (c == ':') { + if (param == 1 && sub == 0) + mod = cur; + cur = -1; + sub++; + } else if ((c >= 'A' && c <= 'D') || c == 'F' || c == 'H' || + c == 'P' || c == 'Q' || c == 'S' || c == '~') { + if (param == 0) + number = cur; + else if (param == 1 && sub == 0) + mod = cur; + else if (param == 1 && sub > 0) + action = cur; + term = c; + i++; + break; + } else { + return PARSE_ERR; + } + i++; + } + + if (term == 0) + return PARSE_NEED_MORE; + + uint16_t key = csi_legacy_key(term, number > 0 ? number : 0); + if (key == 0) + return PARSE_ERR; + + ev->type = EVENT_KEY; + ev->key = key; + ev->mod = kitty_mod(mod > 0 ? mod : 1); + if (action > 0) + ev->action = (uint8_t)action; + + shift(st, i); + return PARSE_OK; +} + /* ── Cap table (xterm defaults) ───────────────────────────────────── */ struct CapEntry { @@ -769,6 +943,22 @@ int input_scan(struct InputState *st, const char *buf, int len, double now) { } } + /* try Kitty-enhanced legacy CSI (arrows, fn keys, etc.) */ + { + struct InputEvent kev; + memset(&kev, 0, sizeof(kev)); + int rv = parse_csi_legacy(st, &kev); + if (rv == PARSE_OK) { + struct InputEvent *ev = emit(st); + *ev = kev; + st->esc_time = 0; + continue; + } + if (rv == PARSE_NEED_MORE) { + return accepted; + } + } + /* unrecognized ESC sequence: treat as Alt + next byte */ shift(st, 1); st->esc_time = 0; diff --git a/src/input.h b/src/input.h index b1859d0..3254da3 100644 --- a/src/input.h +++ b/src/input.h @@ -103,6 +103,42 @@ #define KEY_PGDN 0xFFEA #define KEY_BACKTAB 0xFFE9 +/* Numpad */ +#define KEY_NUMPAD_0 0xFFE2 +#define KEY_NUMPAD_1 0xFFE1 +#define KEY_NUMPAD_2 0xFFE0 +#define KEY_NUMPAD_3 0xFFDF +#define KEY_NUMPAD_4 0xFFDE +#define KEY_NUMPAD_5 0xFFDD +#define KEY_NUMPAD_6 0xFFDC +#define KEY_NUMPAD_7 0xFFDB +#define KEY_NUMPAD_8 0xFFDA +#define KEY_NUMPAD_9 0xFFD9 +#define KEY_NUMPAD_DECIMAL 0xFFD8 +#define KEY_NUMPAD_DIVIDE 0xFFD7 +#define KEY_NUMPAD_MULTIPLY 0xFFD6 +#define KEY_NUMPAD_SUBTRACT 0xFFD5 +#define KEY_NUMPAD_ADD 0xFFD4 +#define KEY_NUMPAD_ENTER 0xFFD3 +#define KEY_NUMPAD_EQUAL 0xFFD2 + +/* Modifier keys */ +#define KEY_SHIFT_LEFT 0xFFD1 +#define KEY_SHIFT_RIGHT 0xFFD0 +#define KEY_CONTROL_LEFT 0xFFCF +#define KEY_CONTROL_RIGHT 0xFFCE +#define KEY_ALT_LEFT 0xFFCD +#define KEY_ALT_RIGHT 0xFFCC +#define KEY_SUPER_LEFT 0xFFCB +#define KEY_SUPER_RIGHT 0xFFCA +#define KEY_HYPER_LEFT 0xFFC9 +#define KEY_HYPER_RIGHT 0xFFC8 +#define KEY_META_LEFT 0xFFC7 +#define KEY_META_RIGHT 0xFFC6 +#define KEY_CAPS_LOCK 0xFFC5 +#define KEY_NUM_LOCK 0xFFC4 +#define KEY_SCROLL_LOCK 0xFFC3 + /* Mouse */ #define KEY_MOUSE_LEFT 0xFFE8 #define KEY_MOUSE_RIGHT 0xFFE7 diff --git a/test/input.test.ts b/test/input.test.ts index c22820e..3b7deff 100644 --- a/test/input.test.ts +++ b/test/input.test.ts @@ -473,20 +473,20 @@ describe("input", () => { expect(result.events[0]).toMatchObject({ type: "keydown", key: "a", + code: "a", alt: true, shifted: "A", - base: "a", }); }); - it("parses empty shifted with base key", () => { + it("parses empty shifted with base key as code", () => { let result = input.scan(str("\x1b[1057::99;5u")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ type: "keydown", key: "\u0421", + code: "c", ctrl: true, - base: "c", }); }); }); @@ -544,6 +544,69 @@ describe("input", () => { }); }); + describe("Kitty legacy CSI sequences", () => { + it("parses arrow up with action", () => { + let result = input.scan(str("\x1b[1;1:1A")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "ArrowUp", + code: "ArrowUp", + }); + }); + + it("parses arrow up release", () => { + let result = input.scan(str("\x1b[1;1:3A")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keyup", + key: "ArrowUp", + code: "ArrowUp", + }); + }); + + it("parses arrow with shift modifier", () => { + let result = input.scan(str("\x1b[1;2:1D")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "ArrowLeft", + code: "ArrowLeft", + shift: true, + }); + }); + + it("parses plain arrow without action", () => { + let result = input.scan(str("\x1b[1;1A")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "ArrowUp", + code: "ArrowUp", + }); + }); + + it("parses delete with action", () => { + let result = input.scan(str("\x1b[3;1:1~")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "Delete", + code: "Delete", + }); + }); + + it("parses F1 with action", () => { + let result = input.scan(str("\x1b[1;1:1P")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "F1", + code: "F1", + }); + }); + }); + describe("UTF-8", () => { it("parses 2-byte UTF-8 (é)", () => { let result = input.scan(bytes(0xc3, 0xa9)); -- 2.51.2