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",