diff --git a/demo/keyboard.ts b/demo/keyboard.ts index bb43b74..e06b61b 100644 --- a/demo/keyboard.ts +++ b/demo/keyboard.ts @@ -5,6 +5,7 @@ import { fixed, grow, type InputEvent, + type KeyEvent, type Op, open, rgba, @@ -29,14 +30,17 @@ interface KeyDef { match: (event: InputEvent) => boolean; } +function isKeyEvent(e: InputEvent): e is KeyEvent { + return e.type === "keydown" || e.type === "keyrepeat" || e.type === "keyup"; +} + function is(char: string): (event: InputEvent) => boolean { return (e) => - (e.type === "char" || e.type === "key") && - e.key.toUpperCase() === char.toUpperCase(); + isKeyEvent(e) && e.key.toUpperCase() === char.toUpperCase(); } function mod(name: "ctrl" | "alt" | "shift"): (event: InputEvent) => boolean { - return (e) => (e.type === "char" || e.type === "key") && e[name] === true; + return (e) => isKeyEvent(e) && e[name] === true; } function never(): boolean { @@ -497,7 +501,7 @@ await main(function* () { Deno.stdout.writeSync(term.render(keyboard(context))); for (let event of yield* each(input)) { - if (event.type === "char" && event.ctrl && event.key === "c") { + if (event.type === "keydown" && event.ctrl && event.key === "c") { break; } @@ -538,13 +542,13 @@ function* inputmode(context: AppContext): Mode { let event = context.event ? context.event : yield context; while (true) { context = { ...context, event }; - if (event.type === "char" && event.key === "x" && event.ctrl) { + if (event.type === "keydown" && event.key === "x" && event.ctrl) { let next = yield context; context = { ...context, event: next, }; - if (next.type === "char" && next.key === "x" && next.ctrl) { + if (next.type === "keydown" && next.key === "x" && next.ctrl) { return configmode({ ...context, event: null, @@ -559,10 +563,10 @@ function* configmode(context: AppContext): Mode { context = { ...context, mode: "config" }; let event = yield context; while (true) { - if (event.type === "key" && event.key === "Escape") { + if (event.type === "keydown" && event.key === "Escape") { return inputmode({...context, event: null }); } - if (event.type === "char") { + if (event.type === "keydown") { switch (event.key) { case "1": { context = {...context, ["Disambiguate escape codes"]: !context["Disambiguate escape codes"]}; diff --git a/input-native.ts b/input-native.ts index 86aa009..b86ce07 100644 --- a/input-native.ts +++ b/input-native.ts @@ -43,7 +43,9 @@ export const KEY_TAB = 0x09; export const KEY_BACKSPACE = 0x7F; export const KEY_SPACE = 0x20; -import { int32, offsets, struct, uint8, uint16, uint32 } from "./typedef.ts"; +import { array, int32, offsets, struct, uint8, uint16, uint32 } from "./typedef.ts"; + +const MAX_TEXT_CODEPOINTS = 8; const InputEventLayout = struct({ type: uint8(), @@ -54,6 +56,11 @@ const InputEventLayout = struct({ y: int32(), w: int32(), h: int32(), + action: uint8(), + shifted: uint32(), + base: uint32(), + text: array(uint32(), MAX_TEXT_CODEPOINTS), + text_len: uint8(), }); const { @@ -65,6 +72,11 @@ const { y: OFFSET_Y, w: OFFSET_W, h: OFFSET_H, + action: OFFSET_ACTION, + text_len: OFFSET_TEXT_LEN, + shifted: OFFSET_SHIFTED, + base: OFFSET_BASE, + text: OFFSET_TEXT, } = offsets(InputEventLayout); export interface NativeInputEvent { @@ -76,9 +88,18 @@ export interface NativeInputEvent { y: number; w: number; h: number; + action: number; + shifted: number; + base: number; + text: number[]; } export function readEvent(view: DataView, ptr: number): NativeInputEvent { + let len = view.getUint8(ptr + OFFSET_TEXT_LEN); + let text: number[] = []; + for (let i = 0; i < len && i < MAX_TEXT_CODEPOINTS; i++) { + text.push(view.getUint32(ptr + OFFSET_TEXT + i * 4, true)); + } return { type: view.getUint8(ptr + OFFSET_TYPE), mod: view.getUint8(ptr + OFFSET_MOD), @@ -88,6 +109,10 @@ export function readEvent(view: DataView, ptr: number): NativeInputEvent { y: view.getInt32(ptr + OFFSET_Y, true), w: view.getInt32(ptr + OFFSET_W, true), h: view.getInt32(ptr + OFFSET_H, true), + action: view.getUint8(ptr + OFFSET_ACTION), + shifted: view.getUint32(ptr + OFFSET_SHIFTED, true), + base: view.getUint32(ptr + OFFSET_BASE, true), + text, }; } diff --git a/input.ts b/input.ts index a86f7ae..ef391c1 100644 --- a/input.ts +++ b/input.ts @@ -56,10 +56,7 @@ import { } from "./input-native.ts"; /** - * Modifier keys held during a key or mouse event. For example, Ctrl+C will be seen as - * ```ts - * { type: "char"; key: "c"; ctrl: true} - * ``` + * Modifier keys held during a key or mouse event. */ export interface KeyModifiers { alt?: true; @@ -68,48 +65,45 @@ export interface KeyModifiers { } /** - * A printable character (Unicode codepoint) was pressed. There may + * Shared key information present on all keyboard events. */ -export interface CharEvent extends KeyModifiers { - type: "char"; +export interface KeyInfo extends KeyModifiers { key: string; } /** - * Non printable keys such as "Escape" + * A key was pressed. Emitted at all enhancement levels. + * On legacy terminals, this is the only keyboard event type. */ -export interface KeyEvent extends KeyModifiers { - type: "key"; - key: - | "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"; +export interface KeyDown extends KeyInfo { + type: "keydown"; + shifted?: string; + base?: string; + text?: string; } +/** + * A key is being held down (auto-repeat). Only emitted with + * Kitty enhancement level 2+ (report event types). + */ +export interface KeyRepeat extends KeyInfo { + type: "keyrepeat"; + shifted?: string; + base?: string; + text?: string; +} + +/** + * A key was released. Only emitted with Kitty enhancement + * level 2+ (report event types). Does not carry text, + * shifted, or base fields. + */ +export interface KeyUp extends KeyInfo { + type: "keyup"; +} + +export type KeyEvent = KeyDown | KeyRepeat | KeyUp; + /** * A mouse button was pressed or released */ @@ -198,7 +192,6 @@ export interface ResizeEvent { } export type InputEvent = - | CharEvent | KeyEvent | MouseEvent | DragEvent @@ -328,7 +321,7 @@ export async function createInput(options: InputOptions = {}): Promise { }; } -const KEY_NAMES = new Map([ +const KEY_NAMES = new Map([ [KEY_F1, "F1"], [KEY_F2, "F2"], [KEY_F3, "F3"], @@ -373,31 +366,58 @@ function mods(native: NativeInputEvent): KeyModifiers { return m; } +function keyName(native: NativeInputEvent): string { + let name = KEY_NAMES.get(native.key); + if (name) { + return name; + } else if (native.key === 0 && native.ch > 0) { + return String.fromCodePoint(native.ch); + } else if (native.key > 0 && native.key < 0x20) { + return String.fromCharCode(native.key + 0x60); + } else { + return String.fromCodePoint(native.ch || native.key); + } +} + +function textFromNative(native: NativeInputEvent): string | undefined { + if (native.text.length === 0) { + return undefined; + } else { + return String.fromCodePoint(...native.text); + } +} + +function mapKeyEvent(native: NativeInputEvent): KeyEvent { + let key = keyName(native); + let m = mods(native); + let isChar = !KEY_NAMES.has(native.key); + let text = textFromNative(native); + + if (native.action === 3) { + return { type: "keyup", key, ...m }; + } + + let type: "keydown" | "keyrepeat" = native.action === 2 + ? "keyrepeat" + : "keydown"; + + let ev: KeyDown | KeyRepeat = { type, key, ...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) { + ev.text = String.fromCodePoint(native.ch); + } + + return ev; +} + function mapEvent(native: NativeInputEvent): InputEvent { switch (native.type) { case EVENT_KEY: { - let name = KEY_NAMES.get(native.key); - if (name) { - return { type: "key", key: name, ...mods(native) }; - } else if (native.key === 0 && native.ch > 0) { - return { - type: "char", - key: String.fromCodePoint(native.ch), - ...mods(native), - }; - } else if (native.key > 0 && native.key < 0x20) { - return { - type: "char", - key: String.fromCharCode(native.key + 0x60), - ...mods(native), - }; - } else { - return { - type: "char", - key: String.fromCodePoint(native.ch || native.key), - ...mods(native), - }; - } + return mapKeyEvent(native); } case EVENT_MOUSE: { if ( @@ -437,11 +457,7 @@ function mapEvent(native: NativeInputEvent): InputEvent { return { type: "resize", width: native.w, height: native.h }; } default: { - return { - type: "char", - key: String.fromCodePoint(native.ch || native.key), - ...mods(native), - }; + return mapKeyEvent(native); } } } diff --git a/src/input.c b/src/input.c index b68daf9..c6886df 100644 --- a/src/input.c +++ b/src/input.c @@ -300,21 +300,31 @@ static uint8_t kitty_mod(int mod) { } static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { - /* \x1b[ codepoint ; modifiers u or \x1b[ codepoint u */ + /* CSI key:shifted:base ; mod:action ; text u */ if (st->len < 2) return PARSE_NEED_MORE; if (st->buf[0] != '\x1b' || st->buf[1] != '[') return PARSE_ERR; if (st->len < 4) return PARSE_NEED_MORE; - if (st->buf[2] < '0' || st->buf[2] > '9') + if (st->buf[2] < '0' && st->buf[2] != ';') + return PARSE_ERR; + if (st->buf[2] > '9' && st->buf[2] != ';') return PARSE_ERR; - int cp = -1; - int mod = -1; + /* fields[param][sub]: up to 3 params, up to 3 sub-fields each */ + int fields[3][3]; + for (int p = 0; p < 3; p++) + for (int s = 0; s < 3; s++) + fields[p][s] = -1; + + int param = 0; + int sub = 0; int cur = -1; int i = 2; int done = 0; + int text_start = -1; + int text_end = -1; while (i < st->len && !done) { char c = st->buf[i]; @@ -322,28 +332,26 @@ static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { if (cur == -1) cur = 0; cur = cur * 10 + (c - '0'); - } else if (c == ';' && cp == -1 && cur != -1) { - cp = cur; + } else if (c == ':') { + if (param < 3 && sub < 3) + fields[param][sub] = cur; cur = -1; - } else if (c == 'u' && cur != -1) { - if (cp == -1) { - cp = cur; - mod = 1; - } else { - mod = cur; + sub++; + if (param == 2 && text_start == -1) + text_start = i + 1; + } else if (c == ';') { + if (param < 3 && sub < 3) + fields[param][sub] = cur; + cur = -1; + param++; + sub = 0; + } else if (c == 'u') { + if (param < 3 && sub < 3) + fields[param][sub] = cur; + if (param == 2 && text_start != -1) { + text_end = i; } done = 1; - } else if (c == ':' && cur != -1) { - /* sub-field separator (event-type, alternate keys) — skip */ - if (cp == -1) { - cp = cur; - } - cur = -1; - /* consume until next ';' or 'u' */ - i++; - while (i < st->len && st->buf[i] != ';' && st->buf[i] != 'u') - i++; - continue; } else { return PARSE_ERR; } @@ -353,6 +361,14 @@ static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { if (!done) return PARSE_NEED_MORE; + int cp = fields[0][0]; + int mod = fields[1][0]; + + if (cp == -1) + cp = 0; + if (mod == -1) + mod = 1; + ev->type = EVENT_KEY; ev->mod = kitty_mod(mod); @@ -363,6 +379,52 @@ static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { ev->ch = (uint32_t)cp; } + /* action */ + if (fields[1][1] > 0) { + ev->action = (uint8_t)fields[1][1]; + } + + /* alternate keys */ + if (fields[0][1] > 0) { + ev->shifted = (uint32_t)fields[0][1]; + } + if (fields[0][2] > 0) { + ev->base = (uint32_t)fields[0][2]; + } + + /* associated text: decode codepoints from param 2 */ + if (param >= 2 && fields[2][0] >= 0) { + /* find the start of param 2 by counting semicolons */ + int off = 2; + int sc = 0; + for (int j = 2; j < i; j++) { + if (st->buf[j] == ';') { + sc++; + if (sc == 2) { + off = j + 1; + break; + } + } + } + /* parse colon-separated codepoints */ + int tc = 0; + int val = -1; + for (int j = off; j < i - 1 && tc < MAX_TEXT_CODEPOINTS; j++) { + char c = st->buf[j]; + if (c >= '0' && c <= '9') { + if (val == -1) val = 0; + val = val * 10 + (c - '0'); + } else if (c == ':') { + if (val >= 0) + ev->text[tc++] = (uint32_t)val; + val = -1; + } + } + if (val >= 0 && tc < MAX_TEXT_CODEPOINTS) + ev->text[tc++] = (uint32_t)val; + ev->text_len = (uint8_t)tc; + } + shift(st, i); return PARSE_OK; } diff --git a/src/input.h b/src/input.h index 4fc8540..b1859d0 100644 --- a/src/input.h +++ b/src/input.h @@ -67,6 +67,13 @@ #define MOD_MOTION 8 #define MOD_RELEASE 16 +/* ── Kitty action types ──────────────────────────────────────────── */ + +#define ACTION_NONE 0 +#define ACTION_PRESS 1 +#define ACTION_REPEAT 2 +#define ACTION_RELEASE 3 + /* ── Key codes ────────────────────────────────────────────────────── */ /* Function keys */ @@ -121,11 +128,19 @@ * @field key KEY_* constant for special keys, raw byte for control chars, * or 0 when the event is a printable character (see ch). * @field ch Unicode codepoint for printable characters, 0 otherwise. - * @field x Mouse column (0-based). Only valid for EVENT_MOUSE. - * @field y Mouse row (0-based). Only valid for EVENT_MOUSE. - * @field w Terminal width. Only valid for EVENT_RESIZE. - * @field h Terminal height. Only valid for EVENT_RESIZE. + * @field x Mouse column (0-based). Only valid for EVENT_MOUSE. + * @field y Mouse row (0-based). Only valid for EVENT_MOUSE. + * @field w Terminal width. Only valid for EVENT_RESIZE. + * @field h Terminal height. Only valid for EVENT_RESIZE. + * @field action Kitty event type: ACTION_NONE/PRESS/REPEAT/RELEASE. + * @field shifted Shifted key codepoint (Kitty alternate keys). + * @field base Base layout key codepoint (Kitty alternate keys). + * @field text_len Number of valid codepoints in text[] (0-8). + * @field text Associated text codepoints (Kitty enhancement level 16+). */ + +#define MAX_TEXT_CODEPOINTS 8 + struct InputEvent { uint8_t type; uint8_t mod; @@ -135,6 +150,11 @@ struct InputEvent { int32_t y; int32_t w; int32_t h; + uint8_t action; + uint32_t shifted; + uint32_t base; + uint32_t text[MAX_TEXT_CODEPOINTS]; + uint8_t text_len; }; /** diff --git a/test/input.test.ts b/test/input.test.ts index c645cb0..c22820e 100644 --- a/test/input.test.ts +++ b/test/input.test.ts @@ -20,14 +20,14 @@ describe("input", () => { it("parses a single character", () => { let result = input.scan(bytes(0x61)); // 'a' expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "char", key: "a" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "a" }); }); it("parses multiple characters", () => { let result = input.scan(str("hello")); expect(result.events.length).toBe(5); - expect(result.events[0]).toMatchObject({ type: "char", key: "h" }); - expect(result.events[4]).toMatchObject({ type: "char", key: "o" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "h" }); + expect(result.events[4]).toMatchObject({ type: "keydown", key: "o" }); }); }); @@ -36,7 +36,7 @@ describe("input", () => { let result = input.scan(bytes(0x03)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "char", + type: "keydown", key: "c", ctrl: true, }); @@ -45,13 +45,13 @@ describe("input", () => { it("parses Tab without ctrl modifier", () => { let result = input.scan(bytes(0x09)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "Tab" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "Tab" }); }); it("parses Enter without ctrl modifier", () => { let result = input.scan(bytes(0x0d)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "Enter" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "Enter" }); }); }); @@ -59,7 +59,7 @@ describe("input", () => { it("parses 0x7f as backspace", () => { let result = input.scan(bytes(0x7f)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "Backspace" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "Backspace" }); }); }); @@ -67,13 +67,13 @@ describe("input", () => { it("parses arrow up (CSI)", () => { let result = input.scan(bytes(0x1b, 0x5b, 0x41)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "ArrowUp" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); }); it("parses arrow down (SS3)", () => { let result = input.scan(bytes(0x1b, 0x4f, 0x42)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "ArrowDown" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "ArrowDown" }); }); }); @@ -82,7 +82,7 @@ describe("input", () => { let result = input.scan(bytes(0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x41)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "ArrowUp", ctrl: true, }); @@ -92,7 +92,7 @@ describe("input", () => { let result = input.scan(bytes(0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x41)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "ArrowUp", shift: true, }); @@ -102,7 +102,7 @@ describe("input", () => { let result = input.scan(bytes(0x1b, 0x5b, 0x31, 0x3b, 0x38, 0x41)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "ArrowUp", ctrl: true, alt: true, @@ -115,13 +115,13 @@ describe("input", () => { it("parses F1 (SS3)", () => { let result = input.scan(bytes(0x1b, 0x4f, 0x50)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "F1" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "F1" }); }); it("parses F5 (CSI ~)", () => { let result = input.scan(str("\x1b[15~")); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "F5" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "F5" }); }); }); @@ -129,26 +129,26 @@ describe("input", () => { it("parses Home", () => { let result = input.scan(bytes(0x1b, 0x4f, 0x48)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "Home" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "Home" }); }); it("parses End", () => { let result = input.scan(bytes(0x1b, 0x4f, 0x46)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "End" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "End" }); }); it("parses Delete", () => { let result = input.scan(str("\x1b[3~")); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "Delete" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "Delete" }); }); it("parses PgDn with Ctrl", () => { let result = input.scan(str("\x1b[6;5~")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "PageDown", ctrl: true, }); @@ -264,7 +264,7 @@ describe("input", () => { await new Promise((r) => setTimeout(r, 30)); let result = input.scan(new Uint8Array(0)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "key", key: "Escape" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "Escape" }); expect(result.pending).toBeUndefined(); }); @@ -274,7 +274,7 @@ describe("input", () => { let r2 = input.scan(bytes(0x5b, 0x41)); expect(r2.events.length).toBe(1); - expect(r2.events[0]).toMatchObject({ type: "key", key: "ArrowUp" }); + expect(r2.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); expect(r2.pending).toBeUndefined(); }); @@ -286,7 +286,7 @@ describe("input", () => { // "x" arrives — ESC + x = Alt+x let r2 = input.scan(bytes(0x78)); // 'x' expect(r2.events.length).toBe(1); - expect(r2.events[0]).toMatchObject({ type: "char", key: "x", alt: true }); + expect(r2.events[0]).toMatchObject({ type: "keydown", key: "x", alt: true }); expect(r2.pending).toBeUndefined(); }); @@ -298,9 +298,9 @@ describe("input", () => { // rest of ArrowUp + "hi" let r2 = input.scan(bytes(0x5b, 0x41, 0x68, 0x69)); expect(r2.events.length).toBe(3); - expect(r2.events[0]).toMatchObject({ type: "key", key: "ArrowUp" }); - expect(r2.events[1]).toMatchObject({ type: "char", key: "h" }); - expect(r2.events[2]).toMatchObject({ type: "char", key: "i" }); + expect(r2.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); + expect(r2.events[1]).toMatchObject({ type: "keydown", key: "h" }); + expect(r2.events[2]).toMatchObject({ type: "keydown", key: "i" }); expect(r2.pending).toBeUndefined(); }); }); @@ -310,7 +310,7 @@ describe("input", () => { let result = input.scan(bytes(0x1b, 0x61)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "char", + type: "keydown", key: "a", alt: true, }); @@ -324,10 +324,10 @@ describe("input", () => { bytes(0x1b, 0x5b, 0x41, 0x1b, 0x5b, 0x42, 0x68, 0x69), ); expect(result.events.length).toBe(4); - expect(result.events[0]).toMatchObject({ type: "key", key: "ArrowUp" }); - expect(result.events[1]).toMatchObject({ type: "key", key: "ArrowDown" }); - expect(result.events[2]).toMatchObject({ type: "char", key: "h" }); - expect(result.events[3]).toMatchObject({ type: "char", key: "i" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); + expect(result.events[1]).toMatchObject({ type: "keydown", key: "ArrowDown" }); + expect(result.events[2]).toMatchObject({ type: "keydown", key: "h" }); + expect(result.events[3]).toMatchObject({ type: "keydown", key: "i" }); }); }); @@ -338,7 +338,7 @@ describe("input", () => { let r2 = input.scan(bytes(0x41)); expect(r2.events.length).toBe(1); - expect(r2.events[0]).toMatchObject({ type: "key", key: "ArrowUp" }); + expect(r2.events[0]).toMatchObject({ type: "keydown", key: "ArrowUp" }); }); }); @@ -347,7 +347,7 @@ describe("input", () => { let result = input.scan(str("\x1b[97;3u")); // a + Alt expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "char", + type: "keydown", key: "a", alt: true, }); @@ -357,7 +357,7 @@ describe("input", () => { let result = input.scan(str("\x1b[9;3u")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "Tab", alt: true, }); @@ -367,7 +367,7 @@ describe("input", () => { let result = input.scan(str("\x1b[13;5u")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "Enter", ctrl: true, }); @@ -377,7 +377,7 @@ describe("input", () => { let result = input.scan(str("\x1b[27;2u")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "Escape", shift: true, }); @@ -387,7 +387,7 @@ describe("input", () => { let result = input.scan(str("\x1b[127u")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "Backspace", }); }); @@ -396,7 +396,7 @@ describe("input", () => { let result = input.scan(str("\x1b[105;7u")); // i + Ctrl+Alt expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "char", + type: "keydown", key: "i", ctrl: true, alt: true, @@ -407,7 +407,7 @@ describe("input", () => { let result = input.scan(str("\x1b[57376;2u")); // F1 + Shift expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "F1", shift: true, }); @@ -417,31 +417,151 @@ describe("input", () => { let result = input.scan(str("\x1b[57352;5u")); // ArrowUp + Ctrl expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "key", + type: "keydown", key: "ArrowUp", ctrl: true, }); }); }); + describe("Kitty action types", () => { + it("parses press as keydown", () => { + let result = input.scan(str("\x1b[97;1:1u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "a" }); + }); + + it("parses repeat as keyrepeat", () => { + let result = input.scan(str("\x1b[97;1:2u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ type: "keyrepeat", key: "a" }); + }); + + it("parses release as keyup", () => { + let result = input.scan(str("\x1b[97;1:3u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ type: "keyup", key: "a" }); + }); + + it("parses F1 press with shift", () => { + let result = input.scan(str("\x1b[57376;2:1u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "F1", + shift: true, + }); + }); + }); + + describe("Kitty alternate keys", () => { + it("parses shifted key", () => { + let result = input.scan(str("\x1b[97:65;6u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "a", + ctrl: true, + shift: true, + shifted: "A", + }); + }); + + it("parses shifted and base keys", () => { + let result = input.scan(str("\x1b[97:65:97;3:1u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "a", + alt: true, + shifted: "A", + base: "a", + }); + }); + + it("parses empty shifted with base key", () => { + let result = input.scan(str("\x1b[1057::99;5u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "\u0421", + ctrl: true, + base: "c", + }); + }); + }); + + describe("Kitty associated text", () => { + it("parses single codepoint text", () => { + let result = input.scan(str("\x1b[97;;97u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "a", + text: "a", + }); + }); + + it("parses text with shift modifier", () => { + let result = input.scan(str("\x1b[97;2;65u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "a", + shift: true, + text: "A", + }); + }); + + it("parses IME text event", () => { + let result = input.scan(str("\x1b[0;;228u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "\0", + text: "\u00e4", + }); + }); + + it("parses multi-codepoint text", () => { + let result = input.scan(str("\x1b[0;;4354:4449:4523u")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "keydown", + key: "\0", + text: "\u1102\u1161\u11AB", + }); + }); + + it("keyup has no text", () => { + let result = input.scan(str("\x1b[97;1:3u")); + expect(result.events.length).toBe(1); + let ev = result.events[0]; + expect(ev).toMatchObject({ type: "keyup", key: "a" }); + expect("text" in ev).toBe(false); + expect("shifted" in ev).toBe(false); + expect("base" in ev).toBe(false); + }); + }); + describe("UTF-8", () => { it("parses 2-byte UTF-8 (é)", () => { let result = input.scan(bytes(0xc3, 0xa9)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "char", key: "\u00e9" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "\u00e9" }); }); it("parses 3-byte UTF-8 (中)", () => { let result = input.scan(bytes(0xe4, 0xb8, 0xad)); expect(result.events.length).toBe(1); - expect(result.events[0]).toMatchObject({ type: "char", key: "\u4e2d" }); + expect(result.events[0]).toMatchObject({ type: "keydown", key: "\u4e2d" }); }); it("parses 4-byte UTF-8 (emoji 🎉)", () => { let result = input.scan(bytes(0xf0, 0x9f, 0x8e, 0x89)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "char", + type: "keydown", key: "\u{1f389}", }); }); diff --git a/typedef.ts b/typedef.ts index c145aae..baa7a08 100644 --- a/typedef.ts +++ b/typedef.ts @@ -28,7 +28,25 @@ export type Num = { T?: T; }; -export type TypeDef = Num | Struct; +export type Arr = { + type: "array"; + element: TypeDef; + length: number; + byteLength: number; + byteAlign: Alignment; +}; + +export type TypeDef = Num | Struct | Arr; + +export function array(element: TypeDef, length: number): Arr { + return { + type: "array", + element: element as TypeDef, + length, + byteLength: element.byteLength * length, + byteAlign: element.byteAlign, + }; +} export const int32 = (): TypeDef => ({ type: "int32",