Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
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
83 changes: 61 additions & 22 deletions input-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,39 +169,83 @@ export interface InputNative {
delay(st: number): number;
}

/**
* Attachment surface provided by a TermInfo handle: the shared memory,
* its bump allocator, and the capability struct / raw terminfo region
* pointers. See terminfo.ts internals().
*/
export interface InputAttach {
memory: WebAssembly.Memory;
exports: Record<string, CallableFunction>;
structPtr: number;
bytesPtr: number;
bytesLen: number;
alloc(size: number, align?: number): number;
}

import { compiled } from "./wasm.ts";

export async function createInputNative(
escLatency: number,
attach?: InputAttach,
): Promise<InputNative> {
let memory = new WebAssembly.Memory({ initial: 4 });

let instance = await WebAssembly.instantiate(compiled, {
env: { memory },
clay: {
measureTextFunction() {},
queryScrollOffsetFunction(ret: number) {
let v = new DataView(memory.buffer);
v.setFloat32(ret, 0, true);
v.setFloat32(ret + 4, 0, true);
let memory = attach?.memory ?? new WebAssembly.Memory({ initial: 4 });

let raw: unknown;
if (attach) {
// Reuse the handle's instance: instantiating the module again over
// the shared memory would rewrite its data segments and clobber
// static state already initialized there.
raw = attach.exports;
} else {
let instance = await WebAssembly.instantiate(compiled, {
env: { memory },
clay: {
measureTextFunction() {},
queryScrollOffsetFunction(ret: number) {
let v = new DataView(memory.buffer);
v.setFloat32(ret, 0, true);
v.setFloat32(ret + 4, 0, true);
},
},
},
});
});
raw = instance.exports;
}

let exports = instance.exports as unknown as {
let exports = raw as {
__heap_base: WebAssembly.Global;
input_size(): number;
input_init(mem: number, escLatency: number): number;
input_init(
mem: number,
escLatency: number,
terminfo: number,
terminfoLen: number,
ti: number,
): number;
input_scan(st: number, buf: number, len: number, now: number): number;
input_count(st: number): number;
input_event(st: number, index: number): number;
input_delay(st: number): number;
};

let heap = exports.__heap_base.value as number;
let size = exports.input_size();
let state = exports.input_init(heap, escLatency);
let buffer = (heap + size + 7) & ~7;
let state: number;
let buffer: number;
if (attach) {
let arena = attach.alloc(size);
buffer = attach.alloc(SCAN_BUFFER_SIZE);
state = exports.input_init(
arena,
escLatency,
attach.bytesLen > 0 ? attach.bytesPtr : 0,
attach.bytesLen,
attach.structPtr,
);
} else {
let heap = exports.__heap_base.value as number;
state = exports.input_init(heap, escLatency, 0, 0, 0);
buffer = (heap + size + 7) & ~7;
}

return {
memory,
Expand All @@ -214,11 +258,6 @@ export async function createInputNative(
};
}

// Compiled terminfo entries are limited to 4096 bytes (legacy) or 32768
// bytes (extended ncurses format). We use the extended limit as our upper
// bound. See https://man7.org/linux/man-pages/man5/term.5.html
export const MAX_TERMINFO = 32768;

// Must match SCAN_BUFFER_SIZE in input.c — the maximum bytes input_scan()
// can accept in a single call.
export const SCAN_BUFFER_SIZE = 4096;
31 changes: 19 additions & 12 deletions input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ import {
KEY_SUPER_LEFT,
KEY_SUPER_RIGHT,
KEY_TAB,
MAX_TERMINFO,
MOD_ALT,
MOD_CTRL,
MOD_MOTION,
Expand All @@ -87,6 +86,8 @@ import {
readEvent,
SCAN_BUFFER_SIZE,
} from "./input-native.ts";
import type { InputAttach } from "./input-native.ts";
import { internals, type TermInfo } from "./terminfo.ts";

/**
* Modifier keys held during a key or mouse event.
Expand Down Expand Up @@ -438,26 +439,32 @@ export interface InputOptions {
escLatency?: number;

/**
* Compiled terminfo binary to load terminal-specific escape sequences.
* TermInfo handle from queryTermInfo(). Attaches the parser to the
* handle's shared memory and capability struct: terminal-specific key
* sequences from the handle's terminfo entry are loaded into the
* sequence trie, and recognized capability query responses are
* written into the shared struct (see specs/terminfo-spec.md).
*
* This is the format used by files like /usr/lib/terminfo/78/xterm-256color
* and they can be directly loaded from disk into this option.
*
* If no terminfo is provided it will use xterm capabilities as the default
* If no handle is provided the parser uses xterm defaults and a
* private capability struct.
*/
terminfo?: Uint8Array;
terminfo?: TermInfo;
}

export async function createInput(options: InputOptions = {}): Promise<Input> {
let { escLatency = 25, terminfo } = options;

if (terminfo && terminfo.byteLength > MAX_TERMINFO) {
throw new RangeError(
`terminfo exceeds ${MAX_TERMINFO} byte limit (got ${terminfo.byteLength})`,
);
let attach: InputAttach | undefined;
if (terminfo) {
let native = internals(terminfo);
if (native.inputAttached) {
throw new Error("TermInfo handle is already attached to an Input");
}
native.inputAttached = true;
attach = native;
}

let native = await createInputNative(escLatency);
let native = await createInputNative(escLatency, attach);

return {
scan(bytes: Uint8Array = new Uint8Array(0)): ScanResult {
Expand Down
12 changes: 12 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@ export * from "./term.ts";
export * from "./input.ts";
export * from "./settings.ts";
export * from "./termcodes.ts";
// terminfo.ts also exports internals(), the attachment surface for
// createTerm/createInput — deliberately not re-exported here.
export {
type Capabilities,
MAX_TERMINFO,
type ProbeInput,
type ProbeOutput,
queryTermInfo,
type QueryTermInfoOptions,
type Rgb,
type TermInfo,
} from "./terminfo.ts";
78 changes: 65 additions & 13 deletions specs/input-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ This specification describes Clayterm's terminal input parsing surface: the API
for decoding raw terminal byte sequences into structured events.

Input parsing is architecturally independent from rendering (see
[Renderer Specification](renderer-spec.md), INV-8). The two concerns share a
[Renderer Specification](renderer-spec.md), INV-7). The two concerns share a
compiled WASM binary for loading efficiency, but neither depends on the other's
state, types, or API surface.
state, types, or API surface. Both consume the shared capability layer defined
in the [Terminfo Specification](terminfo-spec.md); the input parser is
additionally that layer's runtime write path (see Section 6).

This specification is currently non-normative. The input API has clear design
intent but has undergone more revision than the rendering core and faces known
upcoming forces that will reshape it (Kitty progressive enhancement field
surfacing, terminfo binary parsing). It is written to document the current
surface and guide future stabilization.
This specification is currently non-normative except where noted. The input API
has clear design intent but has undergone more revision than the rendering core
and faces known upcoming forces that will reshape it (Kitty progressive
enhancement field surfacing). It is written to document the current surface and
guide future stabilization.

---

Expand All @@ -31,6 +33,8 @@ surface and guide future stabilization.
- The scan API and its return type
- The `InputEvent` discriminated union and its variants
- The ESC timeout resolution model
- Terminfo integration: key sequence loading and capability query response
recognition (Section 6, normative)

### Out of scope

Expand Down Expand Up @@ -74,8 +78,16 @@ Options:
responsiveness (lower values) and correct disambiguation of ESC-prefixed
sequences (higher values).

- **`terminfo`** — A `Uint8Array` of raw terminfo binary. Accepted but C-side
parsing is not yet implemented.
- **`terminfo`** — A `TermInfo` handle from `queryTermInfo()` (see
[Terminfo Specification](terminfo-spec.md) §10). Attaches the parser to the
handle's shared memory and capability struct. Terminal-specific key sequences
from the handle's terminfo bytes are loaded into the parser's sequence trie at
initialization (Section 6.1), and the parser becomes the capability struct's
runtime writer (Section 6.2). When omitted, the parser operates standalone
with xterm default sequences and a private capability struct.

The previous `Uint8Array` form of this option is replaced by the handle form;
raw bytes are supplied via `queryTermInfo({ terminfo: bytes })`.

### 4.2 Scan

Expand Down Expand Up @@ -135,7 +147,50 @@ has already been extended with fields that are not yet mapped to the TS types).

---

## 6. Deferred / Future Areas
## 6. Terminfo Integration

_This section is normative. It defines the input parser's two roles in the
capability layer specified by the [Terminfo Specification](terminfo-spec.md)._

### 6.1 Key sequences from terminfo

When attached to a `TermInfo` handle whose terminfo bytes are present, the
parser MUST load the terminal's `key_*` string capabilities into its escape
sequence trie at initialization, before any scan. Terminfo-supplied sequences
take precedence over the built-in xterm defaults when they conflict; defaults
remain registered for sequences the terminfo entry does not define.

The key capabilities consumed are the `key_*` string range mapped to existing
`KEY_*` codes: arrows (`kcuu1`, `kcud1`, `kcub1`, `kcuf1`), function keys
(`kf1`–`kf12`), editing keys (`khome`, `kend`, `kich1`, `kdch1`, `kpp`, `knp`),
and backtab (`kcbt`). Key capabilities with no corresponding `KEY_*` code are
ignored.

Strings are read directly from the raw terminfo bytes in the shared region; they
are not copied into the capability struct.

### 6.2 Query response recognition

The parser is the runtime write path for the capability struct. During a normal
scan — with responses potentially interleaved with user input — it MUST
recognize and consume the probe responses listed in Terminfo Specification §9.1:
OSC 10/11/12 theme color reports, OSC 21 kitty color reports, OSC 22 pointer
shape reports, XTGETTCAP DCS replies, DECRPM mode-2026 reports, kitty keyboard
flag reports, kitty graphics APC replies, and the DA1 device attributes report.

For each recognized response the parser updates the corresponding struct fields,
sets the `confirmed` bit, and increments the generation, per Terminfo
Specification §6. Responses are consumed silently: they MUST NOT surface as
`InputEvent`s, and bytes belonging to a recognized response MUST NOT leak into
adjacent events.

When the parser is standalone (no handle), responses are still recognized and
consumed — writing into the parser's private struct — so stray replies never
corrupt the event stream.

---

## 7. Deferred / Future Areas

_These topics are explicitly excluded from this specification. Their omission is
intentional, not an oversight._
Expand All @@ -144,9 +199,6 @@ intentional, not an oversight._
struct has been extended for progressive enhancement fields. The TypeScript
event types have not been updated to surface them.

**Terminfo binary parsing.** The input API accepts a `terminfo` option, but
C-side parsing is not implemented.

**Whether input parsing should be a separate package.** Architecturally
independent from the renderer but currently co-located. The distribution
decision is open.
Expand Down
Loading
Loading