Skip to content

Commit 92b123a

Browse files
committed
πŸ› fix keycode mapping and legacy CSI parsing
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.
1 parent c143098 commit 92b123a

7 files changed

Lines changed: 484 additions & 74 deletions

File tree

β€Ž.gitignoreβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/.agent-shell/
22
/clayterm.wasm
33
/build/
4+
/node_modules/

β€Ždemo/keyboard.tsβ€Ž

Lines changed: 60 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// deno-lint-ignore-file no-fallthrough
12
import { each, ensure, main, until } from "effection";
23
import {
34
close,
@@ -34,18 +35,11 @@ function isKeyEvent(e: InputEvent): e is KeyEvent {
3435
return e.type === "keydown" || e.type === "keyrepeat" || e.type === "keyup";
3536
}
3637

37-
function is(char: string): (event: InputEvent) => boolean {
38+
function is(...codes: string[]): (event: InputEvent) => boolean {
3839
return (e) =>
39-
isKeyEvent(e) && e.key.toUpperCase() === char.toUpperCase();
40+
isKeyEvent(e) && e.type === "keydown" && codes.some((c) => e.code.toUpperCase() === c.toUpperCase());
4041
}
4142

42-
function mod(name: "ctrl" | "alt" | "shift"): (event: InputEvent) => boolean {
43-
return (e) => isKeyEvent(e) && e[name] === true;
44-
}
45-
46-
function never(): boolean {
47-
return false;
48-
}
4943

5044
function key(ops: Op[], k: KeyDef, ctx: AppContext): void {
5145
let bg = ctx.event && k.match(ctx.event) ? active : inactive;
@@ -139,7 +133,7 @@ function mainKeys(ops: Op[], ctx: AppContext): void {
139133
], ctx);
140134

141135
row(ops, [
142-
{ label: "Caps", width: 9, match: never },
136+
{ label: "Caps", width: 9, match: is("CapsLock") },
143137
{ label: "A", match: is("a") },
144138
{ label: "S", match: is("s") },
145139
{ label: "D", match: is("d") },
@@ -155,7 +149,7 @@ function mainKeys(ops: Op[], ctx: AppContext): void {
155149
], ctx);
156150

157151
row(ops, [
158-
{ label: "Shift", width: 11, match: mod("shift") },
152+
{ label: "Shift", width: 11, match: is("ShiftLeft") },
159153
{ label: "Z", match: is("z") },
160154
{ label: "X", match: is("x") },
161155
{ label: "C", match: is("c") },
@@ -166,18 +160,18 @@ function mainKeys(ops: Op[], ctx: AppContext): void {
166160
{ label: ",", match: is(",") },
167161
{ label: ".", match: is(".") },
168162
{ label: "/", match: is("/") },
169-
{ label: "Shift", width: 13, match: mod("shift") },
163+
{ label: "Shift", width: 13, match: is("ShiftRight") },
170164
], ctx);
171165

172166
row(ops, [
173-
{ label: "Ctrl", width: 7, match: mod("ctrl") },
174-
{ label: "Win", width: 6, match: never },
175-
{ label: "Alt", width: 6, match: mod("alt") },
167+
{ label: "Ctrl", width: 7, match: is("ControlLeft") },
168+
{ label: "Win", width: 6, match: is("SuperLeft") },
169+
{ label: "Alt", width: 6, match: is("AltLeft") },
176170
{ label: "", width: 33, match: is(" ") },
177-
{ label: "Alt", width: 6, match: mod("alt") },
178-
{ label: "Win", width: 6, match: never },
179-
{ label: "Menu", width: 6, match: never },
180-
{ label: "Ctrl", width: 7, match: mod("ctrl") },
171+
{ label: "Alt", width: 6, match: is("AltRight") },
172+
{ label: "Win", width: 6, match: is("SuperRight") },
173+
{ label: "Menu", width: 6, match: is() },
174+
{ label: "Ctrl", width: 7, match: is("ControlRight") },
181175
], ctx);
182176

183177
ops.push(close());
@@ -232,10 +226,10 @@ function numpad(ops: Op[], ctx: AppContext): void {
232226
);
233227

234228
row(ops, [
235-
{ label: "Num", width: 6, match: never },
236-
{ label: "/", width: 6, match: never },
237-
{ label: "*", width: 6, match: never },
238-
{ label: "-", width: 6, match: never },
229+
{ label: "Num", width: 6, match: is("NumLock") },
230+
{ label: "/", width: 6, match: is("NumpadDivide") },
231+
{ label: "*", width: 6, match: is("NumpadMultiply") },
232+
{ label: "-", width: 6, match: is("NumpadSubtract") },
239233
], ctx);
240234

241235
// rows 2-3 grouped horizontally so + spans both
@@ -248,14 +242,14 @@ function numpad(ops: Op[], ctx: AppContext): void {
248242
open("box", { layout: { direction: "ttb", gap: GAP } }),
249243
);
250244
row(ops, [
251-
{ label: "7", width: 6, match: never },
252-
{ label: "8", width: 6, match: never },
253-
{ label: "9", width: 6, match: never },
245+
{ label: "7", width: 6, match: is("Numpad7") },
246+
{ label: "8", width: 6, match: is("Numpad8") },
247+
{ label: "9", width: 6, match: is("Numpad9") },
254248
], ctx);
255249
row(ops, [
256-
{ label: "4", width: 6, match: never },
257-
{ label: "5", width: 6, match: never },
258-
{ label: "6", width: 6, match: never },
250+
{ label: "4", width: 6, match: is("Numpad4") },
251+
{ label: "5", width: 6, match: is("Numpad5") },
252+
{ label: "6", width: 6, match: is("Numpad6") },
259253
], ctx);
260254
ops.push(close());
261255

@@ -287,13 +281,13 @@ function numpad(ops: Op[], ctx: AppContext): void {
287281
open("box", { layout: { direction: "ttb", gap: GAP } }),
288282
);
289283
row(ops, [
290-
{ label: "1", width: 6, match: never },
291-
{ label: "2", width: 6, match: never },
292-
{ label: "3", width: 6, match: never },
284+
{ label: "1", width: 6, match: is("Numpad1") },
285+
{ label: "2", width: 6, match: is("Numpad2") },
286+
{ label: "3", width: 6, match: is("Numpad3") },
293287
], ctx);
294288
row(ops, [
295-
{ label: "0", width: 13, match: never },
296-
{ label: ".", width: 6, match: never },
289+
{ label: "0", width: 13, match: is("Numpad0") },
290+
{ label: ".", width: 6, match: is("NumpadDecimal") },
297291
], ctx);
298292
ops.push(close());
299293

@@ -357,7 +351,7 @@ function flagPanel(ops: Op[], ctx: AppContext): void {
357351

358352
ops.push(
359353
open("box", { layout: { height: fixed(1) } }),
360-
text("Keyboard Protocol", { color: highlight }),
354+
text("Keyboard Protocol Level", { color: highlight }),
361355
close(),
362356
);
363357

@@ -401,7 +395,7 @@ function keyboard(ctx: AppContext): Op[] {
401395
let badgeLabel = ctx.mode === "input" ? "input" : "config";
402396
let badgeHint = ctx.mode === "input"
403397
? "Ctrl+X Ctrl+X to enter config"
404-
: "Set flags with keys [1-5], Escape to exit";
398+
: "Set flags with keys [1-5], Enter to save";
405399
ops.push(
406400
open("box", { layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } } }),
407401
open("box", { layout: { padding: { left: 1, right: 1 } }, bg: rgba(60, 60, 60) }),
@@ -474,7 +468,7 @@ function ttyFlags(ctx: AppContext): Uint8Array {
474468
if (ctx["Report alternate keys"]) bits |= 4;
475469
if (ctx["Report all keys as escapes"]) bits |= 8;
476470
if (ctx["Report associated text"]) bits |= 16;
477-
return encoder.encode(`\x1b[=${bits};3u`);
471+
return encoder.encode(`\x1b[<u\x1b[>${bits}u`);
478472
}
479473

480474
await main(function* () {
@@ -489,9 +483,9 @@ await main(function* () {
489483

490484
let term = yield* until(createTerm({ width: columns, height: rows }));
491485

492-
esc("\x1b[?1049h\x1b[?25l");
486+
esc("\x1b[?1049h\x1b[?25l\x1b[>3u");
493487
yield* ensure(() => {
494-
esc("\x1b[?25h\x1b[?1049l");
488+
esc("\x1b[<u\x1b[?25h\x1b[?1049l");
495489
});
496490

497491
let modality = recognizer();
@@ -518,8 +512,8 @@ await main(function* () {
518512
function* recognizer(): Iterator<AppContext, never, InputEvent> {
519513
let current: AppContext = {
520514
mode: "input",
521-
"Disambiguate escape codes": false,
522-
"Report event types": false,
515+
"Disambiguate escape codes": true,
516+
"Report event types": true,
523517
"Report alternate keys": false,
524518
"Report all keys as escapes": false,
525519
"Report associated text": false,
@@ -544,11 +538,12 @@ function* inputmode(context: AppContext): Mode {
544538
context = { ...context, event };
545539
if (event.type === "keydown" && event.key === "x" && event.ctrl) {
546540
let next = yield context;
547-
context = {
548-
...context,
549-
event: next,
550-
};
551-
if (next.type === "keydown" && next.key === "x" && next.ctrl) {
541+
while (next.type !== "keydown") {
542+
context = { ...context, event: next };
543+
next = yield context;
544+
}
545+
context = { ...context, event: next };
546+
if (next.key === "x" && next.ctrl) {
552547
return configmode({
553548
...context,
554549
event: null,
@@ -563,31 +558,30 @@ function* configmode(context: AppContext): Mode {
563558
context = { ...context, mode: "config" };
564559
let event = yield context;
565560
while (true) {
566-
if (event.type === "keydown" && event.key === "Escape") {
561+
if (event.type === "keydown" && event.key === "Enter") {
567562
return inputmode({...context, event: null });
568563
}
569-
if (event.type === "keydown") {
564+
if (event.type === "keydown" && "012345".indexOf(event.key) >= 0) {
565+
context = { ...context };
566+
context["Report associated text"] = false;
567+
context["Report all keys as escapes"] = false;
568+
context["Report alternate keys"] = false;
569+
context["Report event types"] = false;
570+
context["Disambiguate escape codes"] = false;
570571
switch (event.key) {
571-
case "1": {
572-
context = {...context, ["Disambiguate escape codes"]: !context["Disambiguate escape codes"]};
573-
break;
574-
}
575-
case "2": {
576-
context = {...context, ["Report event types"]: !context["Report event types"]};
577-
break;
578-
}
579-
case "3": {
580-
context = {...context, ["Report alternate keys"]: !context["Report alternate keys"]};
581-
break;
582-
}
583-
case "4": {
584-
context = {...context, ["Report all keys as escapes"]: !context["Report all keys as escapes"]};
572+
case "5":
573+
context["Report associated text"] = true;
574+
case "4":
575+
context["Report all keys as escapes"] = true;
576+
case "3":
577+
context["Report alternate keys"] = true;
578+
case "2":
579+
context["Report event types"] = true;
580+
case "1":
581+
context["Disambiguate escape codes"] = true;
585582
break;
586-
}
587-
case "5": {
588-
context = {...context, ["Report associated text"]: !context["Report associated text"]};
583+
case "0":
589584
break;
590-
}
591585
}
592586
}
593587
event = yield context;

β€Žinput-native.tsβ€Ž

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,40 @@ export const KEY_DELETE = 0xFFEC;
3131
export const KEY_PGUP = 0xFFEB;
3232
export const KEY_PGDN = 0xFFEA;
3333
export const KEY_BACKTAB = 0xFFE9;
34+
export const KEY_NUMPAD_0 = 0xFFE2;
35+
export const KEY_NUMPAD_1 = 0xFFE1;
36+
export const KEY_NUMPAD_2 = 0xFFE0;
37+
export const KEY_NUMPAD_3 = 0xFFDF;
38+
export const KEY_NUMPAD_4 = 0xFFDE;
39+
export const KEY_NUMPAD_5 = 0xFFDD;
40+
export const KEY_NUMPAD_6 = 0xFFDC;
41+
export const KEY_NUMPAD_7 = 0xFFDB;
42+
export const KEY_NUMPAD_8 = 0xFFDA;
43+
export const KEY_NUMPAD_9 = 0xFFD9;
44+
export const KEY_NUMPAD_DECIMAL = 0xFFD8;
45+
export const KEY_NUMPAD_DIVIDE = 0xFFD7;
46+
export const KEY_NUMPAD_MULTIPLY = 0xFFD6;
47+
export const KEY_NUMPAD_SUBTRACT = 0xFFD5;
48+
export const KEY_NUMPAD_ADD = 0xFFD4;
49+
export const KEY_NUMPAD_ENTER = 0xFFD3;
50+
export const KEY_NUMPAD_EQUAL = 0xFFD2;
51+
52+
export const KEY_SHIFT_LEFT = 0xFFD1;
53+
export const KEY_SHIFT_RIGHT = 0xFFD0;
54+
export const KEY_CONTROL_LEFT = 0xFFCF;
55+
export const KEY_CONTROL_RIGHT = 0xFFCE;
56+
export const KEY_ALT_LEFT = 0xFFCD;
57+
export const KEY_ALT_RIGHT = 0xFFCC;
58+
export const KEY_SUPER_LEFT = 0xFFCB;
59+
export const KEY_SUPER_RIGHT = 0xFFCA;
60+
export const KEY_HYPER_LEFT = 0xFFC9;
61+
export const KEY_HYPER_RIGHT = 0xFFC8;
62+
export const KEY_META_LEFT = 0xFFC7;
63+
export const KEY_META_RIGHT = 0xFFC6;
64+
export const KEY_CAPS_LOCK = 0xFFC5;
65+
export const KEY_NUM_LOCK = 0xFFC4;
66+
export const KEY_SCROLL_LOCK = 0xFFC3;
67+
3468
export const KEY_MOUSE_LEFT = 0xFFE8;
3569
export const KEY_MOUSE_RIGHT = 0xFFE7;
3670
export const KEY_MOUSE_MIDDLE = 0xFFE6;

0 commit comments

Comments
Β (0)