[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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661/* 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 "terminfo.h"#include "trie.h"
/* CAP_COLORDEPTH value encoding (must match input-native.ts): */#define COLORDEPTH_16 0#define COLORDEPTH_256 1#define COLORDEPTH_TRUECOLOR 2#include "mem.h"#include "utf8.h"
#define SCAN_BUFFER_SIZE 4096#define MAX_EVENTS 128/* Longest capability query response we will buffer before giving up. */#define MAX_RESPONSE 1024#define MAX_CSI_PARAM 99999/* Longest terminfo key sequence loaded into the trie (input-spec 6.1). */#define MAX_TERMINFO_KEY 16
#define TCAP_RGB_DENIED 1#define TCAP_TC_DENIED 2#define TCAP_CONFIRMED 4
/* ── 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; int colors; uint8_t tcap; 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;}
/* ── Capability query responses (terminfo-spec section 9) ─────────── */
/* Emit a boolean capability event (type=EVENT_CAPABILITY, key=cap_key, * ch=1 for true / 0 for false). */static void emit_cap_bool(struct InputState *st, uint8_t cap_key, int on) { struct InputEvent *ev = emit(st); ev->type = EVENT_CAPABILITY; ev->key = cap_key; ev->ch = on ? 1 : 0;}
/* Emit an Rgb color capability event (ch = packed 0x00RRGGBB). */static void emit_cap_color(struct InputState *st, uint8_t cap_key, uint32_t color) { struct InputEvent *ev = emit(st); ev->type = EVENT_CAPABILITY; ev->key = cap_key; ev->ch = color;}
static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1;}
/* Scale a 1-4 hex digit channel to 8-bit (rounded). */static int color_channel(const char *s, int n, uint32_t *out) { if (n < 1 || n > 4) return 0; int v = 0; for (int i = 0; i < n; i++) { int h = hexval(s[i]); if (h < 0) return 0; v = v * 16 + h; } int max = (1 << (4 * n)) - 1; *out = (uint32_t)((v * 255 + max / 2) / max); return 1;}
/* Parse an OSC color payload: X11 "rgb:R/G/B" (1-4 hex digits per * channel, "rgba:" alpha ignored) or "#"-hash with equal-width * channels. Returns 0 when no color is recognized. */static int parse_color_spec(const char *s, int len, uint32_t *out) { if (len >= 4 && s[0] == 'r' && s[1] == 'g' && s[2] == 'b') { int i = 3; int alpha = i < len && s[i] == 'a'; if (alpha) i++; if (i >= len || s[i] != ':') return 0; i++; uint32_t ch[3]; for (int c = 0; c < 3; c++) { int start = i; while (i < len && hexval(s[i]) >= 0) i++; if (!color_channel(s + start, i - start, &ch[c])) return 0; if (c < 2) { if (i >= len || s[i] != '/') return 0; i++; } } if (alpha) { if (i >= len || s[i] != '/') return 0; int start = ++i; while (i < len && hexval(s[i]) >= 0) i++; uint32_t ignored; if (!color_channel(s + start, i - start, &ignored)) return 0; } if (i != len) return 0; *out = (ch[0] << 16) | (ch[1] << 8) | ch[2]; return 1; } if (len >= 4 && s[0] == '#') { int n = 0; while (1 + n < len && hexval(s[1 + n]) >= 0) n++; if (1 + n != len || n % 3 != 0) return 0; int w = n / 3; uint32_t ch[3]; for (int c = 0; c < 3; c++) { if (!color_channel(s + 1 + c * w, w, &ch[c])) return 0; } *out = (ch[0] << 16) | (ch[1] << 8) | ch[2]; return 1; } return 0;}
static int payload_contains(const char *s, int len, const char *needle) { int n = (int)strlen(needle); for (int i = 0; i + n <= len; i++) { int j = 0; while (j < n && s[i + j] == needle[j]) j++; if (j == n) return 1; } return 0;}
#define ST_NEED_MORE 0#define ST_TOO_LONG -1#define ST_ENDED_EARLY -2
static int ends_response(char c) { uint8_t b = (uint8_t)c; return (b < 0x20 && b != 0x07 && b != 0x1b) || b == 0x7f;}
/* Find the BEL or ST terminator of a response payload starting at * `start` (input-spec section 6.3). Returns the index one past the * terminator, ST_NEED_MORE, ST_TOO_LONG past MAX_RESPONSE, or * ST_ENDED_EARLY with *at set to the byte that cut the response off. */static int find_st(struct InputState *st, int start, int *at) { for (int i = start; i < st->len; i++) { char c = st->buf[i]; if (c == '\x07') return i + 1; if (c == '\x1b') { if (i + 1 >= st->len) return ST_NEED_MORE; if (st->buf[i + 1] == '\\') return i + 2; *at = i; return ST_ENDED_EARLY; } if (ends_response(c)) { *at = i; return ST_ENDED_EARLY; } if (i - start > MAX_RESPONSE) return ST_TOO_LONG; } return ST_NEED_MORE;}
/* OSC 21 payload: ";"-separated key=value pairs. */static void osc21_apply(struct InputState *st, const char *s, int len) { int i = 0; while (i < len) { int start = i; while (i < len && s[i] != ';') i++; int eq = start; while (eq < i && s[eq] != '=') eq++; if (eq < i) { const char *key = s + start; int klen = eq - start; const char *val = s + eq + 1; int vlen = i - eq - 1; uint32_t color; if (vlen > 0 && parse_color_spec(val, vlen, &color)) { if (klen == 10 && payload_contains(key, klen, "foreground")) { emit_cap_color(st, CAP_FOREGROUND_COLOR, color); } else if (klen == 10 && payload_contains(key, klen, "background")) { emit_cap_color(st, CAP_BACKGROUND_COLOR, color); } else if (klen == 6 && payload_contains(key, klen, "cursor")) { emit_cap_color(st, CAP_CURSOR_COLOR, color); } } } if (i < len) i++; }}
/* OSC 10/11/12 theme color reports, OSC 21 kitty color, OSC 22 kitty * pointer shape. Other OSC numbers are not consumed. */static int parse_osc_response(struct InputState *st) { int i = 2; int num = -1; while (i < st->len && st->buf[i] >= '0' && st->buf[i] <= '9') { num = (num == -1 ? 0 : num) * 10 + (st->buf[i] - '0'); if (num > 22) return PARSE_ERR; i++; } if (i >= st->len) return PARSE_NEED_MORE; if (st->buf[i] != ';') return PARSE_ERR; if (num != 10 && num != 11 && num != 12 && num != 21 && num != 22) return PARSE_ERR; i++;
int at = 0; int end = find_st(st, i, &at); if (end == ST_NEED_MORE) return PARSE_NEED_MORE; if (end == ST_ENDED_EARLY) { shift(st, at); return PARSE_OK; } if (end == ST_TOO_LONG) return PARSE_ERR;
int plen = end - i; if (st->buf[end - 1] == '\x07') { plen -= 1; } else { plen -= 2; } const char *payload = st->buf + i;
uint32_t color; switch (num) { case 10: if (parse_color_spec(payload, plen, &color)) emit_cap_color(st, CAP_FOREGROUND_COLOR, color); break; case 11: if (parse_color_spec(payload, plen, &color)) emit_cap_color(st, CAP_BACKGROUND_COLOR, color); break; case 12: if (parse_color_spec(payload, plen, &color)) emit_cap_color(st, CAP_CURSOR_COLOR, color); break; case 21: osc21_apply(st, payload, plen); break; case 22: emit_cap_bool(st, CAP_POINTER_SHAPE, 1); break; }
shift(st, end); return PARSE_OK;}
/* XTGETTCAP reply: DCS 1 + r … ST (valid) or DCS 0 + r … ST (invalid). * We only query RGB (524742) and Tc (5463). A valid reply naming either * confirms truecolor. The static tier is emitted only once both are * denied without a confirmation since the last DA1 fence; an invalid * reply naming neither denies both. */static int parse_dcs_response(struct InputState *st) { if (st->len < 5) return PARSE_NEED_MORE; char ok = st->buf[2]; if ((ok != '0' && ok != '1') || st->buf[3] != '+' || st->buf[4] != 'r') return PARSE_ERR;
int at = 0; int end = find_st(st, 5, &at); if (end == ST_NEED_MORE) return PARSE_NEED_MORE; if (end == ST_ENDED_EARLY) { shift(st, at); return PARSE_OK; } if (end == ST_TOO_LONG) return PARSE_ERR;
const char *payload = st->buf + 5; int plen = end - 5 - (st->buf[end - 1] == '\x07' ? 1 : 2); int rgb = payload_contains(payload, plen, "524742"); int tc = payload_contains(payload, plen, "5463"); if (ok == '1') { if (rgb || tc) { st->tcap |= TCAP_CONFIRMED; struct InputEvent *ev = emit(st); ev->type = EVENT_CAPABILITY; ev->key = CAP_COLORDEPTH; ev->ch = COLORDEPTH_TRUECOLOR; } } else if (!(st->tcap & TCAP_CONFIRMED)) { uint8_t before = st->tcap; if (rgb || !tc) st->tcap |= TCAP_RGB_DENIED; if (tc || !rgb) st->tcap |= TCAP_TC_DENIED; uint8_t both = TCAP_RGB_DENIED | TCAP_TC_DENIED; if ((st->tcap & both) == both && (before & both) != both) { struct InputEvent *ev = emit(st); ev->type = EVENT_CAPABILITY; ev->key = CAP_COLORDEPTH; ev->ch = (st->colors <= 16) ? COLORDEPTH_16 : COLORDEPTH_256; } }
shift(st, end); return PARSE_OK;}
/* Kitty graphics reply: APC _G … ST with ";OK" on success. */static int parse_apc_response(struct InputState *st) { if (st->len < 3) return PARSE_NEED_MORE; if (st->buf[2] != 'G') return PARSE_ERR;
int at = 0; int end = find_st(st, 3, &at); if (end == ST_NEED_MORE) return PARSE_NEED_MORE; if (end == ST_ENDED_EARLY) { shift(st, at); return PARSE_OK; } if (end == ST_TOO_LONG) return PARSE_ERR;
const char *payload = st->buf + 3; int plen = end - 3 - (st->buf[end - 1] == '\x07' ? 1 : 2); emit_cap_bool(st, CAP_KITTY_GRAPHICS, payload_contains(payload, plen, ";OK"));
shift(st, end); return PARSE_OK;}
/* CSI ? … replies: kitty keyboard flags (final 'u'), DECRPM (final 'y' * with '$' intermediate), DA1 device attributes (final 'c'). */static int parse_csi_private(struct InputState *st) { if (st->len < 3) return PARSE_NEED_MORE; if (st->buf[2] != '?') return PARSE_ERR;
int nums[4] = {-1, -1, -1, -1}; int ni = 0; int cur = -1; char intermediate = 0; int i = 3;
while (i < st->len) { if (i - 3 > MAX_RESPONSE) return PARSE_ERR; char c = st->buf[i]; if (c >= '0' && c <= '9') { if (cur == -1) cur = 0; if (cur <= MAX_CSI_PARAM) cur = cur * 10 + (c - '0'); } else if (c == ';' || c == ':') { if (ni < 4) nums[ni++] = cur; cur = -1; } else if (c >= 0x20 && c <= 0x2f) { intermediate = c; } else if (c >= 0x40 && c <= 0x7e) { if (ni < 4) nums[ni++] = cur; i++;
if (c == 'u' && intermediate == 0) { emit_cap_bool(st, CAP_KITTY_KEYBOARD, 1); } else if (c == 'y' && intermediate == '$') { if (nums[0] == 2026 && ni >= 2) { int v = nums[1]; emit_cap_bool(st, CAP_SYNC_OUTPUT, v == 1 || v == 2 || v == 3); } /* other modes: consumed silently */ } else if (c == 'c' && intermediate == 0) { /* DA1 fence: consumed silently, MUST NOT surface as CapabilityEvent */ st->tcap = 0; } else { return PARSE_ERR; }
shift(st, i); return PARSE_OK; } else { return PARSE_ERR; } i++; } return PARSE_NEED_MORE;}
static int parse_response(struct InputState *st) { if (st->len < 2) return PARSE_NEED_MORE; switch (st->buf[1]) { case ']': return parse_osc_response(st); case 'P': return parse_dcs_response(st); case '_': return parse_apc_response(st); case '[': return parse_csi_private(st); default: return PARSE_ERR; }}
/* ── 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 ───────────────────────────────────────────────────── */
/* terminfo key_* string capability indices (ncurses Caps) mapped to * KEY_* codes. Indices verified against tic output. */struct KeyCap { uint16_t index; uint16_t key;};
static const struct KeyCap key_caps[] = { {59, KEY_DELETE}, /* kdch1 */ {61, KEY_ARROW_DOWN}, /* kcud1 */ {66, KEY_F1}, /* kf1 */ {67, KEY_F10}, /* kf10 */ {68, KEY_F2}, /* kf2 */ {69, KEY_F3}, /* kf3 */ {70, KEY_F4}, /* kf4 */ {71, KEY_F5}, /* kf5 */ {72, KEY_F6}, /* kf6 */ {73, KEY_F7}, /* kf7 */ {74, KEY_F8}, /* kf8 */ {75, KEY_F9}, /* kf9 */ {76, KEY_HOME}, /* khome */ {77, KEY_INSERT}, /* kich1 */ {79, KEY_ARROW_LEFT}, /* kcub1 */ {81, KEY_PGDN}, /* knp */ {82, KEY_PGUP}, /* kpp */ {83, KEY_ARROW_RIGHT}, /* kcuf1 */ {87, KEY_ARROW_UP}, /* kcuu1 */ {148, KEY_BACKTAB}, /* kcbt */ {164, KEY_END}, /* kend */ {216, KEY_F11}, /* kf11 */ {217, KEY_F12}, /* kf12 */ {0, 0},};
int input_size(void) { return align8((int)sizeof(struct InputState)); }
struct InputState *input_init(void *mem, int esc_latency_ms, const uint8_t *terminfo, int terminfo_len, int initial_colors) { struct InputState *st = (struct InputState *)mem; memset(st, 0, sizeof(struct InputState)); st->esc_latency_ms = esc_latency_ms; st->colors = initial_colors > 0 ? initial_colors : 256;
/* build escape sequence trie: terminfo keys first (first writer wins * in trie_add, so the entry's sequences take precedence), then the * xterm defaults for anything the entry does not define */ trie_init(st->trie, &st->trie_len); if (terminfo && terminfo_len > 0) { for (int i = 0; key_caps[i].key; i++) { int n = 0; const char *seq = terminfo_str(terminfo, terminfo_len, key_caps[i].index, &n); if (seq && n > 0 && n <= MAX_TERMINFO_KEY) trie_add(st->trie, &st->trie_len, seq, n, key_caps[i].key, 0); } } 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;}
static int is_introducer(char c) { return c == '[' || c == 'O' || c == ']' || c == 'P' || c == '_';}
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; }
/* ESC + lone introducer: Alt+key or the start of a sequence */ if (st->len == 2 && is_introducer(st->buf[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->mod = MOD_ALT; ev->ch = (uint8_t)st->buf[1]; shift(st, 2); st->esc_time = 0; continue; } 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; } }
/* try capability query responses (OSC/DCS/APC/CSI ?) */ { int rv = parse_response(st); if (rv == PARSE_OK) { 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;}