1# @clo/terminal
2
3Browser-pure terminal emulation on the vendored libghostty-vt WebAssembly core. The package includes a headless terminal, a native-selection DOM renderer, thin React and Solid adapters, explicit CSS, pinned fonts, and the wasm artifact. Transport remains caller-owned: pty bytes go in, terminal-bound bytes come out.
4
5## Install
6
7```sh
8pnpm add @clo/terminal
9```
10
11Add `react` or `solid-js` only when using that adapter. Import the renderer stylesheet once:
12
13```ts
14import "@clo/terminal/terminal.css";
15```
16
17Vite and compatible bundlers can resolve the exported wasm as a URL:
18
19```ts
20import { GhosttyModule } from "@clo/terminal";
21import wasmUrl from "@clo/terminal/ghostty-vt.wasm?url";
22
23const module = await GhosttyModule.fetch(wasmUrl);
24```
25
26`GhosttyModule.load(bytes)` remains available when the host owns artifact loading. One module can host many terminals.
27
28## Vanilla DOM
29
30```ts
31import { mountTerminal } from "@clo/terminal";
32
33const terminal = mountTerminal(document.querySelector("#terminal")!, module, {
34 onData: (bytes) => socket.send(bytes),
35 onResize: ({ cols, rows }) => resizePty(cols, rows),
36});
37
38socket.binaryType = "arraybuffer";
39const receive = ({ data }: MessageEvent<string | ArrayBuffer>) =>
40 terminal.write(typeof data === "string" ? data : new Uint8Array(data));
41socket.addEventListener("message", receive);
42await terminal.ready;
43
44// Later:
45socket.removeEventListener("message", receive);
46terminal.dispose();
47```
48
49`write` parses pty output and schedules a DOM frame. `onData` combines keystrokes, paste, mouse reports, and terminal query responses so all terminal-bound bytes use one transport path.
50
51## React
52
53```tsx
54import { TerminalPane } from "@clo/terminal/react";
55
56<TerminalPane
57 module={module}
58 onData={(bytes) => socket.send(bytes)}
59 onResize={({ cols, rows }) => resizePty(cols, rows)}
60 onReady={(handle) => {
61 socket.binaryType = "arraybuffer";
62 const receive = ({ data }: MessageEvent<string | ArrayBuffer>) =>
63 handle.write(typeof data === "string" ? data : new Uint8Array(data));
64 socket.addEventListener("message", receive);
65 return () => socket.removeEventListener("message", receive);
66 }}
67/>;
68```
69
70## Solid
71
72```tsx
73import { GhosttyModule } from "@clo/terminal";
74import { Show, createSignal } from "solid-js";
75import { TerminalPane } from "@clo/terminal/solid";
76
77const [module, setModule] = createSignal<GhosttyModule>();
78void GhosttyModule.fetch(wasmUrl).then(setModule);
79
80<Show when={module()}>
81 {(loaded) => (
82 <TerminalPane
83 module={loaded()}
84 onData={(bytes) => socket.send(bytes)}
85 onResize={({ cols, rows }) => resizePty(cols, rows)}
86 onReady={(handle) => {
87 socket.binaryType = "arraybuffer";
88 const receive = ({ data }: MessageEvent<string | ArrayBuffer>) =>
89 handle.write(typeof data === "string" ? data : new Uint8Array(data));
90 socket.addEventListener("message", receive);
91 return () => socket.removeEventListener("message", receive);
92 }}
93 />
94 )}
95</Show>;
96```
97
98Both framework components own disposal and remount only when `module` changes. Geometry, parser, and renderer options are mount-time values; callback props remain live. `AnsiText` in each adapter renders a static escaped ANSI snapshot without creating an emulator.
99
100## Public API
101
102| Entry | Surface |
103| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104| `@clo/terminal` | `GhosttyModule`, `mountTerminal`, `Terminal`, `TerminalView`, frame/cell types, ANSI conversion, link detection, key mapping, run helpers, predictive-echo engine, and shell-mark/command utilities |
105| `@clo/terminal/react` | `TerminalPane`, `AnsiText`, and their props |
106| `@clo/terminal/solid` | `TerminalPane`, `AnsiText`, and their props |
107| `@clo/terminal/terminal.css` | DOM renderer structure, pinned font faces, cursor/blink/scrollbar behavior, and CSS theme seams |
108| `@clo/terminal/ghostty-vt.wasm` | pinned libghostty-vt artifact for caller-controlled loading |
109
110### Mounted handle
111
112`mountTerminal` and adapter `onReady` callbacks expose:
113
114- `write(data)` feeds pty output and schedules paint.
115- `ready` resolves after initial fonts, measurement, fit, and resize reporting settle.
116- `terminal` exposes headless state, scrollback, input encoders, snapshots, title/pwd, and shell command history.
117- `view` exposes scheduling, fit, resize observation, metric refresh, inspection, and direct paint controls.
118- `dispose()` releases the renderer lease and wasm resources; framework adapters call it automatically.
119
120### Mount options
121
122The high-level mount and both framework adapters share the same options: initial `cols`/`rows`, byte-bounded scrollback, shell integration and command retention, scrollbar/scrollback/mouse/sticky behavior, links, predictive echo, resize, title/pwd/bell/shell-command events, clipboard policy, and the unified `onData` transport callback.
123
124### Headless and advanced APIs
125
126`Terminal` can run without the DOM. `snapshot()` is a destructive dirty-state read; use `acquireFrameReader()` when attaching a renderer and release it before another consumer. `TerminalView` is exported for custom lifecycle ownership. Pure utilities cover ANSI-to-HTML, canonical cells/runs, URL/path/OSC 8 links, DOM key translation, OSC scanning, shell-mark parsing, command tracking, and sticky-command selection.
127
128## Predictive echo
129
130Over a remote pty, every keystroke waits a round trip before a glyph appears. `predict` paints typed characters speculatively, in the manner of [mosh](https://mosh.org), and reconciles them against real output as it arrives.
131
132```ts
133mountTerminal(element, module, {
134 onData: (bytes) => socket.send(bytes),
135 predict: { mode: "prompt", minLatencyMs: 25 },
136});
137```
138
139`predict: true` is shorthand for the defaults. It is **off** unless you ask for it.
140
141Unlike mosh, this does not guess whether the far end echoes — it requires proof. Predictions run only inside a shell-integration readline region (OSC 133/633 `B` seen, `C` not yet), so an uninstrumented shell, a `sh` session, and every full-screen application predict nothing at all. Within that region only single-column printable ASCII is predicted, and backspace only un-types the engine's own predictions. Wide characters, combining marks, line wraps, paste, mouse input, control chords, and resize all drop the outstanding speculation rather than extend it, and any contradiction drops all of it.
142
143Display is adaptive. Predictions are recorded from the first keystroke but painted only once the smoothed round trip reaches `minLatencyMs` (default 25ms), so a local pty never shows one. Set `minLatencyMs: 0` to predict immediately.
144
145| Option | Default | Meaning |
146| ---------------- | ------- | ---------------------------------------------------------- |
147| `mode` | `off` | `off`, or `prompt` for the readline-region gate |
148| `minLatencyMs` | `25` | Round trip below which predictions are recorded, not shown |
149| `timeoutMs` | `1000` | Unconfirmed prediction lifetime |
150| `underline` | `true` | Mark unconfirmed predictions as speculative |
151| `maxPredictions` | `128` | Cap on outstanding predictions |
152
153Speculative glyphs render as `.clo-term-run.predicted` and are themed with `--clo-term-predicted-opacity` and `--clo-term-predicted-underline`. They never enter terminal state: `Terminal.text()`, snapshots, and the command model only ever describe what the pty actually sent. `view.predictions` exposes the engine for diagnostics, including the measured `latencyMs`.
154
155`PredictionEngine` is also exported standalone. It is pure — plain screen data in, an overlay out, time supplied as an argument — so a different renderer can reuse it.
156
157## Styling
158
159Set theme values on the mounted element or an ancestor:
160
161```css
162.my-terminal {
163 --clo-term-bg: #11111b;
164 --clo-term-fg: #cdd6f4;
165 --clo-term-cursor: #f5e0dc;
166 --clo-term-font-size: 14px;
167 --clo-term-line-height: 1.35;
168 --clo-term-scrollbar-thumb: rgb(205 214 244 / 28%);
169}
170```
171
172Call `handle.view.refreshMetrics()` after changing a loaded font family or metric-affecting CSS variable.
173
174## Security
175
176Links are inert unless `links.onActivate` is supplied. Clipboard writes are denied unless `onClipboardWrite` synchronously returns `true`; the package never reads the clipboard. The terminal performs no navigation, socket, pty, filesystem, Node, Electron, or VS Code work.