[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay bomb.sh/docs/tty
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161/* input.c — VT/ANSI escape sequence parser * * Decodes raw terminal input bytes into structured InputEvent records. * All state lives in an arena-allocated InputState; no libc required. * * Public API (exported to WASM): * input_size — compute arena size * input_init — initialize parser state in provided memory * input_scan — feed bytes, produce events * input_count — number of events from last scan * input_event — pointer to event by index * input_pending — deadline for pending ESC (0 = none) */
#include "input.h"#include "trie.h"#include "mem.h"#include "utf8.h"
#define SCAN_BUFFER_SIZE 4096#define MAX_EVENTS 128
/* ── State ────────────────────────────────────────────────────────── */
struct InputState { int esc_latency_ms; char buf[SCAN_BUFFER_SIZE]; int len; double esc_time; struct InputEvent events[MAX_EVENTS]; int count; int trie_len; Trie trie;};
/* ── Helpers ──────────────────────────────────────────────────────── */
static void shift(struct InputState *st, int n) { st->len -= n; if (st->len > 0) { /* move remaining bytes to front */ for (int i = 0; i < st->len; i++) st->buf[i] = st->buf[i + n]; }}
static struct InputEvent *emit(struct InputState *st) { if (st->count >= MAX_EVENTS) return &st->events[MAX_EVENTS - 1]; /* saturate */ struct InputEvent *ev = &st->events[st->count++]; memset(ev, 0, sizeof(*ev)); return ev;}
/* ── Mouse parsing ────────────────────────────────────────────────── */
#define MOUSE_BUTTON_MASK 3#define MOUSE_BUTTON_LEFT 0#define MOUSE_BUTTON_MIDDLE 1#define MOUSE_BUTTON_RIGHT 2#define MOUSE_BUTTON_RELEASE 3#define MOUSE_WHEEL_BIT 64
static uint8_t mouse_mods(int b) { uint8_t m = 0; if (b & 4) m |= MOD_SHIFT; if (b & 8) m |= MOD_ALT; if (b & 16) m |= MOD_CTRL; if (b & 32) m |= MOD_MOTION; return m;}
static uint16_t mouse_button(int b) { switch (b & MOUSE_BUTTON_MASK) { case MOUSE_BUTTON_LEFT: if (b & MOUSE_WHEEL_BIT) { return KEY_MOUSE_WHEEL_UP; } else { return KEY_MOUSE_LEFT; } case MOUSE_BUTTON_MIDDLE: if (b & MOUSE_WHEEL_BIT) { return KEY_MOUSE_WHEEL_DOWN; } else { return KEY_MOUSE_MIDDLE; } case MOUSE_BUTTON_RIGHT: return KEY_MOUSE_RIGHT; case MOUSE_BUTTON_RELEASE: return KEY_MOUSE_RELEASE; default: return KEY_MOUSE_LEFT; }}
static int parse_mouse_vt200(struct InputState *st, struct InputEvent *ev) { /* \x1b[M + 3 bytes = 6 total */ if (st->len < 6) return PARSE_NEED_MORE; if (st->buf[0] != '\x1b' || st->buf[1] != '[' || st->buf[2] != 'M') return PARSE_ERR;
int b = st->buf[3] - 0x20; ev->type = EVENT_MOUSE; ev->key = mouse_button(b); ev->mod = mouse_mods(b); if (ev->key == KEY_MOUSE_RELEASE) ev->mod |= MOD_RELEASE; ev->x = ((uint8_t)st->buf[4]) - 0x21; ev->y = ((uint8_t)st->buf[5]) - 0x21;
shift(st, 6); return PARSE_OK;}
static int parse_mouse_sgr(struct InputState *st, struct InputEvent *ev) { /* \x1b[< btn ; x ; y {M|m} */ if (st->len < 3) return PARSE_NEED_MORE; if (st->buf[0] != '\x1b' || st->buf[1] != '[' || st->buf[2] != '<') return PARSE_ERR;
int num[3] = {-1, -1, -1}; int ni = 0; int cur = -1; char trail = ' '; int i = 3;
while (i < st->len && ni < 3) { char c = st->buf[i]; if (c >= '0' && c <= '9') { if (cur == -1) cur = 0; cur = cur * 10 + (c - '0'); } else if (cur != -1 && ((ni < 2 && c == ';') || (ni == 2 && (c == 'm' || c == 'M')))) { num[ni++] = cur; cur = -1; trail = c; } else { return PARSE_ERR; } i++; }
if (num[2] == -1) return PARSE_NEED_MORE;
ev->type = EVENT_MOUSE; ev->key = mouse_button(num[0]); ev->mod = mouse_mods(num[0]); if (trail == 'm') ev->mod |= MOD_RELEASE; ev->x = (num[1] - 1 < 0) ? 0 : num[1] - 1; ev->y = (num[2] - 1 < 0) ? 0 : num[2] - 1;
shift(st, i); return PARSE_OK;}
static int parse_mouse_urxvt(struct InputState *st, struct InputEvent *ev) { /* \x1b[ btn ; x ; y M (no '<' prefix, digits start at buf[2]) */ if (st->len < 3) return PARSE_NEED_MORE; if (st->buf[0] != '\x1b' || st->buf[1] != '[') return PARSE_ERR; /* must start with a digit at buf[2] */ if (st->buf[2] < '0' || st->buf[2] > '9') return PARSE_ERR;
int num[3] = {-1, -1, -1}; int ni = 0; int cur = -1; int i = 2;
while (i < st->len && ni < 3) { char c = st->buf[i]; if (c >= '0' && c <= '9') { if (cur == -1) cur = 0; cur = cur * 10 + (c - '0'); } else if (cur != -1 && ((ni < 2 && c == ';') || (ni == 2 && c == 'M'))) { num[ni++] = cur; cur = -1; } else { return PARSE_ERR; } i++; }
if (num[2] == -1) return PARSE_NEED_MORE;
int b = num[0] - 0x20; ev->type = EVENT_MOUSE; ev->key = mouse_button(b); ev->mod = mouse_mods(b); if (ev->key == KEY_MOUSE_RELEASE) ev->mod |= MOD_RELEASE; ev->x = (num[1] - 1 < 0) ? 0 : num[1] - 1; ev->y = (num[2] - 1 < 0) ? 0 : num[2] - 1;
shift(st, i); return PARSE_OK;}
static int parse_mouse(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 rv; if (st->buf[2] == 'M') { rv = parse_mouse_vt200(st, ev); } else if (st->buf[2] == '<') { rv = parse_mouse_sgr(st, ev); } else if (st->buf[2] >= '0' && st->buf[2] <= '9') { rv = parse_mouse_urxvt(st, ev); } else { rv = PARSE_ERR; } return rv;}
/* ── Kitty keyboard protocol (CSI u) ──────────────────────────────── */
static uint16_t kitty_key(int cp) { switch (cp) { case 9: return KEY_TAB; case 13: return KEY_ENTER; case 27: return KEY_ESC; case 127: return KEY_BACKSPACE; case 57344: return KEY_ESC; case 57345: return KEY_ENTER; case 57346: return KEY_TAB; case 57347: return KEY_BACKSPACE; case 57348: return KEY_INSERT; case 57349: return KEY_DELETE; case 57350: return KEY_ARROW_LEFT; case 57351: return KEY_ARROW_RIGHT; case 57352: return KEY_ARROW_UP; case 57353: return KEY_ARROW_DOWN; case 57354: return KEY_PGUP; case 57355: return KEY_PGDN; case 57356: return KEY_HOME; case 57357: return KEY_END; case 57376: return KEY_F1; case 57377: return KEY_F2; case 57378: return KEY_F3; case 57379: return KEY_F4; case 57380: return KEY_F5; case 57381: return KEY_F6; case 57382: return KEY_F7; case 57383: return KEY_F8; case 57384: return KEY_F9; case 57385: return KEY_F10; case 57386: 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; }}
static uint8_t kitty_mod(int mod) { if (mod <= 1) return 0; int bits = mod - 1; uint8_t out = 0; if (bits & 1) out |= MOD_SHIFT; if (bits & 2) out |= MOD_ALT; if (bits & 4) out |= MOD_CTRL; return out;}
static int parse_csi_u(struct InputState *st, struct InputEvent *ev) { /* 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] != ';') return PARSE_ERR; if (st->buf[2] > '9' && st->buf[2] != ';') return PARSE_ERR;
/* 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]; if (c >= '0' && c <= '9') { if (cur == -1) cur = 0; cur = cur * 10 + (c - '0'); } else if (c == ':') { if (param < 3 && sub < 3) fields[param][sub] = cur; cur = -1; 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 { return PARSE_ERR; } i++; }
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);
uint16_t key = kitty_key(cp); if (key) { ev->key = key; } else { 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;}
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 DSR cursor position response: CSI row ; col R */static int parse_cursor(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 row = 0; int col = 0; int param = 0; int i = 2;
while (i < st->len) { char c = st->buf[i]; if (c >= '0' && c <= '9') { if (param == 0) { row = row * 10 + (c - '0'); } else { col = col * 10 + (c - '0'); } } else if (c == ';') { if (param > 0) return PARSE_ERR; param++; } else if (c == 'R') { if (param != 1) return PARSE_ERR; i++; ev->type = EVENT_CURSOR; ev->y = row; ev->x = col; shift(st, i); return PARSE_OK; } else { return PARSE_ERR; } i++; } return PARSE_NEED_MORE;}
/* 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 { const char *seq; uint16_t key; uint8_t mod;};
static const struct CapEntry base_caps[] = { /* xterm base keys (no modifiers) */ {"\x1bOP", KEY_F1, 0}, {"\x1bOQ", KEY_F2, 0}, {"\x1bOR", KEY_F3, 0}, {"\x1bOS", KEY_F4, 0}, {"\x1b[15~", KEY_F5, 0}, {"\x1b[17~", KEY_F6, 0}, {"\x1b[18~", KEY_F7, 0}, {"\x1b[19~", KEY_F8, 0}, {"\x1b[20~", KEY_F9, 0}, {"\x1b[21~", KEY_F10, 0}, {"\x1b[23~", KEY_F11, 0}, {"\x1b[24~", KEY_F12, 0}, {"\x1b[2~", KEY_INSERT, 0}, {"\x1b[3~", KEY_DELETE, 0}, {"\x1bOH", KEY_HOME, 0}, {"\x1bOF", KEY_END, 0}, {"\x1b[5~", KEY_PGUP, 0}, {"\x1b[6~", KEY_PGDN, 0}, {"\x1bOA", KEY_ARROW_UP, 0}, {"\x1bOB", KEY_ARROW_DOWN, 0}, {"\x1bOD", KEY_ARROW_LEFT, 0}, {"\x1bOC", KEY_ARROW_RIGHT, 0}, {"\x1b[Z", KEY_BACKTAB, 0}, /* alternate arrow sequences (CSI, used by many terminals) */ {"\x1b[A", KEY_ARROW_UP, 0}, {"\x1b[B", KEY_ARROW_DOWN, 0}, {"\x1b[D", KEY_ARROW_LEFT, 0}, {"\x1b[C", KEY_ARROW_RIGHT, 0}, /* alternate home/end (CSI ~ style) */ {"\x1b[1~", KEY_HOME, 0}, {"\x1b[4~", KEY_END, 0}, {"\x1b[H", KEY_HOME, 0}, {"\x1b[F", KEY_END, 0}, {0, 0, 0},};
/* xterm modifier combos — modifier code: 2=Shift 3=Alt 4=Alt+Shift * 5=Ctrl 6=Ctrl+Shift 7=Ctrl+Alt 8=Ctrl+Alt+Shift */static const struct CapEntry mod_caps[] = { /* arrows */ {"\x1b[1;2A", KEY_ARROW_UP, MOD_SHIFT}, {"\x1b[1;3A", KEY_ARROW_UP, MOD_ALT}, {"\x1b[1;4A", KEY_ARROW_UP, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5A", KEY_ARROW_UP, MOD_CTRL}, {"\x1b[1;6A", KEY_ARROW_UP, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7A", KEY_ARROW_UP, MOD_CTRL | MOD_ALT}, {"\x1b[1;8A", KEY_ARROW_UP, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2B", KEY_ARROW_DOWN, MOD_SHIFT}, {"\x1b[1;3B", KEY_ARROW_DOWN, MOD_ALT}, {"\x1b[1;4B", KEY_ARROW_DOWN, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5B", KEY_ARROW_DOWN, MOD_CTRL}, {"\x1b[1;6B", KEY_ARROW_DOWN, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7B", KEY_ARROW_DOWN, MOD_CTRL | MOD_ALT}, {"\x1b[1;8B", KEY_ARROW_DOWN, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2C", KEY_ARROW_RIGHT, MOD_SHIFT}, {"\x1b[1;3C", KEY_ARROW_RIGHT, MOD_ALT}, {"\x1b[1;4C", KEY_ARROW_RIGHT, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5C", KEY_ARROW_RIGHT, MOD_CTRL}, {"\x1b[1;6C", KEY_ARROW_RIGHT, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7C", KEY_ARROW_RIGHT, MOD_CTRL | MOD_ALT}, {"\x1b[1;8C", KEY_ARROW_RIGHT, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2D", KEY_ARROW_LEFT, MOD_SHIFT}, {"\x1b[1;3D", KEY_ARROW_LEFT, MOD_ALT}, {"\x1b[1;4D", KEY_ARROW_LEFT, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5D", KEY_ARROW_LEFT, MOD_CTRL}, {"\x1b[1;6D", KEY_ARROW_LEFT, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7D", KEY_ARROW_LEFT, MOD_CTRL | MOD_ALT}, {"\x1b[1;8D", KEY_ARROW_LEFT, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* home/end */ {"\x1b[1;2H", KEY_HOME, MOD_SHIFT}, {"\x1b[1;3H", KEY_HOME, MOD_ALT}, {"\x1b[1;4H", KEY_HOME, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5H", KEY_HOME, MOD_CTRL}, {"\x1b[1;6H", KEY_HOME, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7H", KEY_HOME, MOD_CTRL | MOD_ALT}, {"\x1b[1;8H", KEY_HOME, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2F", KEY_END, MOD_SHIFT}, {"\x1b[1;3F", KEY_END, MOD_ALT}, {"\x1b[1;4F", KEY_END, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5F", KEY_END, MOD_CTRL}, {"\x1b[1;6F", KEY_END, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7F", KEY_END, MOD_CTRL | MOD_ALT}, {"\x1b[1;8F", KEY_END, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* insert */ {"\x1b[2;2~", KEY_INSERT, MOD_SHIFT}, {"\x1b[2;3~", KEY_INSERT, MOD_ALT}, {"\x1b[2;4~", KEY_INSERT, MOD_ALT | MOD_SHIFT}, {"\x1b[2;5~", KEY_INSERT, MOD_CTRL}, {"\x1b[2;6~", KEY_INSERT, MOD_CTRL | MOD_SHIFT}, {"\x1b[2;7~", KEY_INSERT, MOD_CTRL | MOD_ALT}, {"\x1b[2;8~", KEY_INSERT, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* delete */ {"\x1b[3;2~", KEY_DELETE, MOD_SHIFT}, {"\x1b[3;3~", KEY_DELETE, MOD_ALT}, {"\x1b[3;4~", KEY_DELETE, MOD_ALT | MOD_SHIFT}, {"\x1b[3;5~", KEY_DELETE, MOD_CTRL}, {"\x1b[3;6~", KEY_DELETE, MOD_CTRL | MOD_SHIFT}, {"\x1b[3;7~", KEY_DELETE, MOD_CTRL | MOD_ALT}, {"\x1b[3;8~", KEY_DELETE, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* pgup */ {"\x1b[5;2~", KEY_PGUP, MOD_SHIFT}, {"\x1b[5;3~", KEY_PGUP, MOD_ALT}, {"\x1b[5;4~", KEY_PGUP, MOD_ALT | MOD_SHIFT}, {"\x1b[5;5~", KEY_PGUP, MOD_CTRL}, {"\x1b[5;6~", KEY_PGUP, MOD_CTRL | MOD_SHIFT}, {"\x1b[5;7~", KEY_PGUP, MOD_CTRL | MOD_ALT}, {"\x1b[5;8~", KEY_PGUP, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* pgdn */ {"\x1b[6;2~", KEY_PGDN, MOD_SHIFT}, {"\x1b[6;3~", KEY_PGDN, MOD_ALT}, {"\x1b[6;4~", KEY_PGDN, MOD_ALT | MOD_SHIFT}, {"\x1b[6;5~", KEY_PGDN, MOD_CTRL}, {"\x1b[6;6~", KEY_PGDN, MOD_CTRL | MOD_SHIFT}, {"\x1b[6;7~", KEY_PGDN, MOD_CTRL | MOD_ALT}, {"\x1b[6;8~", KEY_PGDN, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* F1–F4 */ {"\x1b[1;2P", KEY_F1, MOD_SHIFT}, {"\x1b[1;3P", KEY_F1, MOD_ALT}, {"\x1b[1;4P", KEY_F1, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5P", KEY_F1, MOD_CTRL}, {"\x1b[1;6P", KEY_F1, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7P", KEY_F1, MOD_CTRL | MOD_ALT}, {"\x1b[1;8P", KEY_F1, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2Q", KEY_F2, MOD_SHIFT}, {"\x1b[1;3Q", KEY_F2, MOD_ALT}, {"\x1b[1;4Q", KEY_F2, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5Q", KEY_F2, MOD_CTRL}, {"\x1b[1;6Q", KEY_F2, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7Q", KEY_F2, MOD_CTRL | MOD_ALT}, {"\x1b[1;8Q", KEY_F2, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2R", KEY_F3, MOD_SHIFT}, {"\x1b[1;3R", KEY_F3, MOD_ALT}, {"\x1b[1;4R", KEY_F3, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5R", KEY_F3, MOD_CTRL}, {"\x1b[1;6R", KEY_F3, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7R", KEY_F3, MOD_CTRL | MOD_ALT}, {"\x1b[1;8R", KEY_F3, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[1;2S", KEY_F4, MOD_SHIFT}, {"\x1b[1;3S", KEY_F4, MOD_ALT}, {"\x1b[1;4S", KEY_F4, MOD_ALT | MOD_SHIFT}, {"\x1b[1;5S", KEY_F4, MOD_CTRL}, {"\x1b[1;6S", KEY_F4, MOD_CTRL | MOD_SHIFT}, {"\x1b[1;7S", KEY_F4, MOD_CTRL | MOD_ALT}, {"\x1b[1;8S", KEY_F4, MOD_CTRL | MOD_ALT | MOD_SHIFT},
/* F5–F12 */ {"\x1b[15;2~", KEY_F5, MOD_SHIFT}, {"\x1b[15;3~", KEY_F5, MOD_ALT}, {"\x1b[15;4~", KEY_F5, MOD_ALT | MOD_SHIFT}, {"\x1b[15;5~", KEY_F5, MOD_CTRL}, {"\x1b[15;6~", KEY_F5, MOD_CTRL | MOD_SHIFT}, {"\x1b[15;7~", KEY_F5, MOD_CTRL | MOD_ALT}, {"\x1b[15;8~", KEY_F5, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[17;2~", KEY_F6, MOD_SHIFT}, {"\x1b[17;3~", KEY_F6, MOD_ALT}, {"\x1b[17;4~", KEY_F6, MOD_ALT | MOD_SHIFT}, {"\x1b[17;5~", KEY_F6, MOD_CTRL}, {"\x1b[17;6~", KEY_F6, MOD_CTRL | MOD_SHIFT}, {"\x1b[17;7~", KEY_F6, MOD_CTRL | MOD_ALT}, {"\x1b[17;8~", KEY_F6, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[18;2~", KEY_F7, MOD_SHIFT}, {"\x1b[18;3~", KEY_F7, MOD_ALT}, {"\x1b[18;4~", KEY_F7, MOD_ALT | MOD_SHIFT}, {"\x1b[18;5~", KEY_F7, MOD_CTRL}, {"\x1b[18;6~", KEY_F7, MOD_CTRL | MOD_SHIFT}, {"\x1b[18;7~", KEY_F7, MOD_CTRL | MOD_ALT}, {"\x1b[18;8~", KEY_F7, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[19;2~", KEY_F8, MOD_SHIFT}, {"\x1b[19;3~", KEY_F8, MOD_ALT}, {"\x1b[19;4~", KEY_F8, MOD_ALT | MOD_SHIFT}, {"\x1b[19;5~", KEY_F8, MOD_CTRL}, {"\x1b[19;6~", KEY_F8, MOD_CTRL | MOD_SHIFT}, {"\x1b[19;7~", KEY_F8, MOD_CTRL | MOD_ALT}, {"\x1b[19;8~", KEY_F8, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[20;2~", KEY_F9, MOD_SHIFT}, {"\x1b[20;3~", KEY_F9, MOD_ALT}, {"\x1b[20;4~", KEY_F9, MOD_ALT | MOD_SHIFT}, {"\x1b[20;5~", KEY_F9, MOD_CTRL}, {"\x1b[20;6~", KEY_F9, MOD_CTRL | MOD_SHIFT}, {"\x1b[20;7~", KEY_F9, MOD_CTRL | MOD_ALT}, {"\x1b[20;8~", KEY_F9, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[21;2~", KEY_F10, MOD_SHIFT}, {"\x1b[21;3~", KEY_F10, MOD_ALT}, {"\x1b[21;4~", KEY_F10, MOD_ALT | MOD_SHIFT}, {"\x1b[21;5~", KEY_F10, MOD_CTRL}, {"\x1b[21;6~", KEY_F10, MOD_CTRL | MOD_SHIFT}, {"\x1b[21;7~", KEY_F10, MOD_CTRL | MOD_ALT}, {"\x1b[21;8~", KEY_F10, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[23;2~", KEY_F11, MOD_SHIFT}, {"\x1b[23;3~", KEY_F11, MOD_ALT}, {"\x1b[23;4~", KEY_F11, MOD_ALT | MOD_SHIFT}, {"\x1b[23;5~", KEY_F11, MOD_CTRL}, {"\x1b[23;6~", KEY_F11, MOD_CTRL | MOD_SHIFT}, {"\x1b[23;7~", KEY_F11, MOD_CTRL | MOD_ALT}, {"\x1b[23;8~", KEY_F11, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{"\x1b[24;2~", KEY_F12, MOD_SHIFT}, {"\x1b[24;3~", KEY_F12, MOD_ALT}, {"\x1b[24;4~", KEY_F12, MOD_ALT | MOD_SHIFT}, {"\x1b[24;5~", KEY_F12, MOD_CTRL}, {"\x1b[24;6~", KEY_F12, MOD_CTRL | MOD_SHIFT}, {"\x1b[24;7~", KEY_F12, MOD_CTRL | MOD_ALT}, {"\x1b[24;8~", KEY_F12, MOD_CTRL | MOD_ALT | MOD_SHIFT},
{0, 0, 0},};
/* ── Public API ───────────────────────────────────────────────────── */
int input_size(void) { return align8((int)sizeof(struct InputState)); }
struct InputState *input_init(void *mem, int esc_latency_ms) { struct InputState *st = (struct InputState *)mem; memset(st, 0, sizeof(struct InputState)); st->esc_latency_ms = esc_latency_ms;
/* build escape sequence trie from cap tables */ trie_init(st->trie, &st->trie_len); for (int i = 0; base_caps[i].seq; i++) trie_add(st->trie, &st->trie_len, base_caps[i].seq, strlen(base_caps[i].seq), base_caps[i].key, base_caps[i].mod); for (int i = 0; mod_caps[i].seq; i++) trie_add(st->trie, &st->trie_len, mod_caps[i].seq, strlen(mod_caps[i].seq), mod_caps[i].key, mod_caps[i].mod);
return st;}
int input_scan(struct InputState *st, const char *buf, int len, double now) {
/* append incoming bytes (may be partial if buffer is full) */ int space = SCAN_BUFFER_SIZE - st->len; int accepted = len < space ? len : space; if (accepted > 0) { memcpy(st->buf + st->len, buf, accepted); st->len += accepted; }
st->count = 0;
while (st->len > 0 && st->count < MAX_EVENTS) { /* ── ESC handling ───────────────────────────────────────────── */ if ((uint8_t)st->buf[0] == 0x1b) { /* lone ESC? */ if (st->len == 1) { if (st->esc_time == 0) st->esc_time = now; if (now - st->esc_time >= (double)st->esc_latency_ms) { struct InputEvent *ev = emit(st); ev->type = EVENT_KEY; ev->key = KEY_ESC; shift(st, 1); st->esc_time = 0; continue; } /* pending — caller should retry after timeout */ return accepted; }
/* try trie match */ { int consumed = 0; uint16_t key = 0; uint8_t mod = 0; int rv = trie_match(st->trie, st->buf, st->len, &consumed, &key, &mod); if (rv == PARSE_OK) { struct InputEvent *ev = emit(st); ev->type = EVENT_KEY; ev->key = key; ev->mod = mod; shift(st, consumed); st->esc_time = 0; continue; } if (rv == PARSE_NEED_MORE) { return accepted; } }
/* try mouse */ { struct InputEvent mev; memset(&mev, 0, sizeof(mev)); int rv = parse_mouse(st, &mev); if (rv == PARSE_OK) { struct InputEvent *ev = emit(st); *ev = mev; st->esc_time = 0; continue; } if (rv == PARSE_NEED_MORE) { return accepted; } }
/* try CSI u (Kitty keyboard protocol) */ { struct InputEvent kev; memset(&kev, 0, sizeof(kev)); int rv = parse_csi_u(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; } }
/* try DSR cursor position response */ { struct InputEvent kev; memset(&kev, 0, sizeof(kev)); int rv = parse_cursor(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; } }
/* 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;
if (st->len > 0) { uint8_t b = (uint8_t)st->buf[0]; struct InputEvent *ev = emit(st); ev->type = EVENT_KEY; ev->mod = MOD_ALT;
if (b < 0x20) { ev->key = b; ev->mod |= MOD_CTRL; shift(st, 1); } else if (b == 0x7f) { ev->key = KEY_BACKSPACE; shift(st, 1); } else if (b >= 0x20 && b < 0x80) { ev->ch = b; shift(st, 1); } else { /* Alt + UTF-8 char */ int need = utf8_len(b); if (need > st->len) { return accepted; /* need more */ } uint32_t cp; utf8_decode(&cp, st->buf); ev->ch = cp; shift(st, need); } } continue; }
/* ── Control characters ─────────────────────────────────────── */ uint8_t b = (uint8_t)st->buf[0];
if (b < 0x20) { struct InputEvent *ev = emit(st); ev->type = EVENT_KEY; ev->key = b; if (b != KEY_TAB && b != KEY_ENTER) { ev->mod = MOD_CTRL; } shift(st, 1); st->esc_time = 0; continue; }
/* ── Backspace (0x7f) ───────────────────────────────────────── */ if (b == 0x7f) { struct InputEvent *ev = emit(st); ev->type = EVENT_KEY; ev->key = KEY_BACKSPACE; shift(st, 1); st->esc_time = 0; continue; }
/* ── Printable / UTF-8 ──────────────────────────────────────── */ { int need = utf8_len(b); if (need > st->len) { return accepted; /* partial UTF-8 */ } uint32_t cp; int n = utf8_decode(&cp, st->buf); if (n <= 0) { n = 1; cp = 0xfffd; } struct InputEvent *ev = emit(st); ev->type = EVENT_KEY; ev->ch = cp; shift(st, n); st->esc_time = 0; } }
return accepted;}
int input_count(struct InputState *st) { return st->count; }
struct InputEvent *input_event(struct InputState *st, int index) { if (index < 0 || index >= st->count) return 0; return &st->events[index];}
int input_delay(struct InputState *st) { if (st->esc_time > 0) { return st->esc_latency_ms; } return 0;}