Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: terminfo capability core (struct, parser, queryTermInfo)
Implements the terminfo-spec capability core:

- src/terminfo.{c,h}: the TermInfo capability struct (generation,
  colors, flags, confirmed, theme group), the xterm-256color baseline
  init, and terminfo_parse for both storage formats (legacy 0432 and
  extended-number 01036) including the extended capability table
  (RGB/Tc/Su/Smulx). Parsing is bounds-checked everywhere and
  all-or-nothing: malformed input returns a nonzero code without
  touching the struct (TINV-3). A successful parse replaces the
  standard capabilities the entry owns — absent booleans clear, colors
  becomes the entry's max_colors — matching ncurses semantics where an
  entry fully describes its terminal. terminfo_grant applies
  creation-time evidence (COLORTERM) and bumps the generation only on
  actual change.

- terminfo.ts: queryTermInfo() as the single blessed entry point.
  Locates entries via the ncurses search path (TERMINFO, ~/.terminfo,
  TERMINFO_DIRS, compiled-in defaults; letter and hex directory
  layouts; traversal-safe, magic-validated — ported from the
  bombshell-dev/ui feat/terminfo spike), parses into a fresh shared
  WebAssembly.Memory with a bump allocator (the renderer and input
  parser will attach to the same memory in follow-ups), applies
  COLORTERM evidence, and runs the sans-IO probe batch (OSC 10/11/12,
  kitty OSC 21/22, XTGETTCAP RGB;Tc, DECRQM 2026, kitty keyboard and
  graphics, DA1 fence) over injectable streams. It never rejects on
  environmental grounds: missing entries, non-TTY streams, timeouts,
  and aborts all resolve with whatever evidence was gathered. Raw mode
  is saved and restored around the probe window.

- test/terminfo.test.ts + embedded fixtures (tasks/gen-fixtures.ts):
  real xterm-256color, tic-compiled extended-caps entry, hand-built
  01036 entry (macOS ships ncurses 6.0, which predates that format),
  16-color downgrade entry. The extended-block layout (name offsets
  relative to the names sub-table) was verified against tic output
  byte-by-byte.

The probe window currently closes on timeout only; DA1 fence
recognition lands with the input parser integration.
  • Loading branch information
natemoo-re committed Aug 21, 2026
commit 255d573d822bab7f745f7dcd122464343b9cee05
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ EXPORTS = \
-Wl,--export=input_scan \
-Wl,--export=input_count \
-Wl,--export=input_event \
-Wl,--export=input_delay
-Wl,--export=input_delay \
-Wl,--export=terminfo_size \
-Wl,--export=terminfo_init \
-Wl,--export=terminfo_parse \
-Wl,--export=terminfo_grant

LDFLAGS = -Wl,--no-entry \
-Wl,--import-memory \
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@bomb.sh/tty",
"license": "MIT",
"tasks": {
"test": "deno test",
"test": "deno test --allow-read --allow-write",
"fmt": "deno fmt && clang-format -i src/*.c src/*.h",
"fmt:check": "deno fmt --check && clang-format --dry-run --Werror src/*.c src/*.h",
"build:npm": "deno run -A tasks/build-npm.ts",
Expand Down
1 change: 1 addition & 0 deletions src/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "../clay/clay.h"

#include "mem.c"
#include "terminfo.c"
#include "buffer.c"
#include "cell.c"
#include "utf8.c"
Expand Down
227 changes: 227 additions & 0 deletions src/terminfo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/* terminfo.c — shared terminal capability layer */

#include "terminfo.h"

#include "mem.h"

#define TI_MAGIC_LEGACY 0x011a
#define TI_MAGIC_EXTENDED 0x021e

/* Standard capability indices (ncurses Caps). */
#define TI_BOOL_AM 1
#define TI_BOOL_XENL 4
#define TI_BOOL_BCE 28
#define TI_NUM_MAX_COLORS 13
#define TI_STR_SMCUP 28

int terminfo_size(void) { return align8(sizeof(struct TermInfo)); }

struct TermInfo *terminfo_init(void *mem) {
struct TermInfo *ti = (struct TermInfo *)mem;
ti->generation = 1;
ti->colors = 256;
ti->flags = TERMINFO_BCE | TERMINFO_AM | TERMINFO_XENL | TERMINFO_ALTSCREEN;
ti->confirmed = 0;
ti->theme_fg = 0;
ti->theme_bg = 0;
ti->theme_cursor = 0;
return ti;
}

void terminfo_grant(struct TermInfo *ti, uint32_t flags) {
if ((ti->flags & flags) == flags)
return;
ti->flags |= flags;
ti->generation++;
}

static uint16_t rd_u16(const uint8_t *b, int off) {
return (uint16_t)(b[off] | (b[off + 1] << 8));
}

/* Signed terminfo number: -1 = absent, -2 = cancelled. */
static int32_t rd_num(const uint8_t *b, int off, int width) {
if (width == 2) {
uint16_t v = rd_u16(b, off);
if (v == 0xffff)
return -1;
if (v == 0xfffe)
return -2;
return (int32_t)v;
}
uint32_t v = (uint32_t)b[off] | ((uint32_t)b[off + 1] << 8) |
((uint32_t)b[off + 2] << 16) | ((uint32_t)b[off + 3] << 24);
return (int32_t)v;
}

static int rd_i16(const uint8_t *b, int off) {
uint16_t v = rd_u16(b, off);
if (v == 0xffff)
return -1;
if (v == 0xfffe)
return -2;
return (int)v;
}

static int ti_strnlen(const uint8_t *b, int max) {
int n = 0;
while (n < max && b[n])
n++;
return n;
}

static int name_is(const uint8_t *table, int table_len, int off,
const char *name) {
if (off < 0 || off >= table_len)
return 0;
int i = 0;
while (name[i]) {
if (off + i >= table_len || table[off + i] != (uint8_t)name[i])
return 0;
i++;
}
return off + i < table_len && table[off + i] == 0;
}

/* Extended capability block (after the standard sections). Returns 0 on
* success, nonzero when the declared structure runs out of bounds.
* Grants flag bits into *flags for RGB/Tc/Su booleans and Smulx. */
static int parse_ext(const uint8_t *b, int len, int off, int numw,
uint32_t *flags) {
if (off & 1)
off++;
if (off + 10 > len)
return 0; /* no extended block */

int eb = rd_i16(b, off);
int en = rd_i16(b, off + 2);
int es = rd_i16(b, off + 4);
int table_strings = rd_i16(b, off + 6);
int table_len = rd_i16(b, off + 8);
if (eb < 0 || en < 0 || es < 0 || table_strings < 0 || table_len < 0)
return 1;

int bools_off = off + 10;
int nums_off = bools_off + eb;
if (nums_off & 1)
nums_off++;
int offsets_off = nums_off + en * numw;
int name_count = eb + en + es;
int names_off = offsets_off + es * 2;
int table_off = names_off + name_count * 2;
if (table_off + table_len > len)
return 1;

const uint8_t *table = b + table_off;

/* Value strings sit at the head of the table; names follow. Name
* offsets are relative to the start of the names sub-table. */
int names_base = 0;
for (int i = 0; i < es; i++) {
int v = rd_i16(b, offsets_off + i * 2);
if (v < 0)
continue;
if (v >= table_len)
return 1;
int end = v + ti_strnlen(table + v, table_len - v) + 1;
if (end > names_base)
names_base = end;
}

for (int i = 0; i < name_count; i++) {
int noff = rd_i16(b, names_off + i * 2);
if (noff < 0)
continue;
noff += names_base;

if (i < eb) {
if (!b[bools_off + i])
continue;
if (name_is(table, table_len, noff, "Tc") ||
name_is(table, table_len, noff, "RGB")) {
*flags |= TERMINFO_TRUECOLOR;
} else if (name_is(table, table_len, noff, "Su")) {
*flags |= TERMINFO_STYLED_UNDERLINE;
}
} else if (i >= eb + en) {
int v = rd_i16(b, offsets_off + (i - eb - en) * 2);
if (v < 0)
continue;
if (name_is(table, table_len, noff, "Smulx")) {
*flags |= TERMINFO_STYLED_UNDERLINE;
}
}
}

return 0;
}

int terminfo_parse(const uint8_t *bytes, int len, struct TermInfo *ti) {
if (len < 12)
return 1;

uint16_t magic = rd_u16(bytes, 0);
int numw;
if (magic == TI_MAGIC_LEGACY) {
numw = 2;
} else if (magic == TI_MAGIC_EXTENDED) {
numw = 4;
} else {
return 2;
}

int name_size = rd_i16(bytes, 2);
int bool_count = rd_i16(bytes, 4);
int num_count = rd_i16(bytes, 6);
int str_count = rd_i16(bytes, 8);
int table_len = rd_i16(bytes, 10);
if (name_size < 0 || bool_count < 0 || num_count < 0 || str_count < 0 ||
table_len < 0)
return 3;

int bools_off = 12 + name_size;
int nums_off = bools_off + bool_count;
if (nums_off & 1)
nums_off++;
int strs_off = nums_off + num_count * numw;
int table_off = strs_off + str_count * 2;
int end = table_off + table_len;
if (end > len)
return 4;

uint32_t flags = 0;
if (bool_count > TI_BOOL_AM && bytes[bools_off + TI_BOOL_AM])
flags |= TERMINFO_AM;
if (bool_count > TI_BOOL_XENL && bytes[bools_off + TI_BOOL_XENL])
flags |= TERMINFO_XENL;
if (bool_count > TI_BOOL_BCE && bytes[bools_off + TI_BOOL_BCE])
flags |= TERMINFO_BCE;

int32_t colors = 0;
if (num_count > TI_NUM_MAX_COLORS) {
int32_t v = rd_num(bytes, nums_off + TI_NUM_MAX_COLORS * numw, numw);
if (v > 0)
colors = v;
}
if (colors >= (1 << 24))
flags |= TERMINFO_TRUECOLOR;

if (str_count > TI_STR_SMCUP) {
int v = rd_i16(bytes, strs_off + TI_STR_SMCUP * 2);
if (v >= 0 && v < table_len)
flags |= TERMINFO_ALTSCREEN;
}

if (parse_ext(bytes, len, end, numw, &flags))
return 5;

/* The entry describes the terminal completely for the capabilities it
* owns: replace them, leave probe-only flags and theme fields alone. */
uint32_t keep =
~(TERMINFO_TRUECOLOR | TERMINFO_BCE | TERMINFO_AM | TERMINFO_XENL |
TERMINFO_ALTSCREEN | TERMINFO_STYLED_UNDERLINE);
ti->flags = (ti->flags & keep) | flags;
ti->colors = (uint32_t)colors;
ti->generation++;
return 0;
}
82 changes: 82 additions & 0 deletions src/terminfo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* terminfo.h — shared terminal capability layer
*
* Implements the capability struct and terminfo binary parsing defined
* by specs/terminfo-spec.md. The renderer reads the struct; the input
* parser writes probe responses into it; this module owns the baseline
* and the parse path.
*/

#ifndef TERMINFO_H
#define TERMINFO_H

#include <stdint.h>

/* Capability flag bits (terminfo-spec section 6). */
#define TERMINFO_TRUECOLOR (1u << 0)
#define TERMINFO_BCE (1u << 1)
#define TERMINFO_AM (1u << 2)
#define TERMINFO_XENL (1u << 3)
#define TERMINFO_ALTSCREEN (1u << 4)
#define TERMINFO_STYLED_UNDERLINE (1u << 5)
#define TERMINFO_SYNC (1u << 6)
#define TERMINFO_KITTY_KEYBOARD (1u << 7)
#define TERMINFO_KITTY_GRAPHICS (1u << 8)
#define TERMINFO_KITTY_COLOR (1u << 9)
#define TERMINFO_HYPERLINKS (1u << 10)
#define TERMINFO_POINTER_SHAPE (1u << 11)
#define TERMINFO_THEME_FG (1u << 12)
#define TERMINFO_THEME_BG (1u << 13)
#define TERMINFO_THEME_CURSOR (1u << 14)

struct TermInfo {
uint32_t generation;
uint32_t colors;
uint32_t flags;
uint32_t confirmed;
uint32_t theme_fg;
uint32_t theme_bg;
uint32_t theme_cursor;
};

/**
* Return the number of bytes needed to hold a TermInfo struct.
*/
int terminfo_size(void);

/**
* Initialize a capability struct to the xterm-256color baseline
* (terminfo-spec section 7.1). Generation starts at 1.
*
* @param mem Pointer to at least terminfo_size() bytes.
* @return The initialized struct.
*/
struct TermInfo *terminfo_init(void *mem);

/**
* Parse a compiled terminfo entry into a capability struct.
*
* Supports the legacy (0432) and extended number (01036) storage
* formats, including the extended capability table (RGB, Tc, Su,
* Smulx). All reads are bounds-checked. On any malformed input the
* struct is left untouched (all-or-nothing, TINV-3).
*
* On success the entry's standard capabilities replace the baseline:
* booleans absent from the entry are cleared, colors becomes the
* entry's max_colors (0 when the entry does not define it), and the
* generation is bumped once.
*
* @param bytes Compiled terminfo entry.
* @param len Byte length.
* @param ti Struct to populate.
* @return 0 on success, nonzero parse-result code on failure.
*/
int terminfo_parse(const uint8_t *bytes, int len, struct TermInfo *ti);

/**
* Grant capability flag bits from evidence collected outside the
* parser (environment evidence at handle creation, e.g. COLORTERM).
* Bumps the generation only when the flags actually change.
*/
void terminfo_grant(struct TermInfo *ti, uint32_t flags);

#endif
Loading