···11+# Building clayterm from source
22+33+This guide is for maintainers and builders working on clayterm itself.
44+55+It covers:
66+77+- cloning the repo correctly,
88+- initializing the `clay` git submodule,
99+- installing the toolchain needed to compile the C sources to WebAssembly,
1010+- building the local development artifacts, and
1111+- verifying that the repo is ready for development.
1212+1313+It does **not** cover npm/JSR packaging or publishing.
1414+1515+## What the local build produces
1616+1717+The local source build is driven by `make`.
1818+1919+It generates:
2020+2121+- `clayterm.wasm` — the compiled WebAssembly module built from the C sources
2222+- `wasm.ts` — a generated TypeScript file derived from `clayterm.wasm`
2323+2424+`wasm.ts` is generated output, not hand-maintained source.
2525+2626+## Clone the repo with submodules
2727+2828+The build depends on the `clay` git submodule.
2929+3030+Preferred fresh clone:
3131+3232+```sh
3333+git clone --recurse-submodules https://github.com/bombshell-dev/clayterm.git
3434+cd clayterm
3535+```
3636+3737+If you already cloned without submodules:
3838+3939+```sh
4040+git submodule update --init --recursive
4141+```
4242+4343+Quick check:
4444+4545+```sh
4646+git submodule status --recursive
4747+```
4848+4949+You should also see a populated `clay/` directory. If `clay/` is missing or
5050+empty, fix the submodule state before building.
5151+5252+## Required tools
5353+5454+You need:
5555+5656+- `git`
5757+- `make`
5858+- `clang` with wasm32-capable support
5959+- `deno`
6060+6161+Equivalent packages are fine if your package manager uses different names.
6262+6363+## Install the toolchain
6464+6565+### macOS
6666+6767+Install Apple's command line tools first. They provide the base developer tools,
6868+including `git` and `make`.
6969+7070+```sh
7171+xcode-select --install
7272+```
7373+7474+Then install LLVM and Deno with Homebrew:
7575+7676+```sh
7777+brew install llvm deno
7878+```
7979+8080+Use Homebrew LLVM before Apple's system `clang` when building clayterm:
8181+8282+```sh
8383+echo 'export PATH="$(brew --prefix llvm)/bin:$PATH"' >> ~/.zshrc
8484+source ~/.zshrc
8585+```
8686+8787+If you do not already have `git` available after installing the command line
8888+tools, install it with Homebrew:
8989+9090+```sh
9191+brew install git
9292+```
9393+9494+### Debian / Ubuntu
9595+9696+Install the build toolchain and Git:
9797+9898+```sh
9999+sudo apt-get update
100100+sudo apt-get install -y build-essential clang lld git curl
101101+```
102102+103103+Install Deno with the official installer:
104104+105105+```sh
106106+curl -fsSL https://deno.land/install.sh | sh
107107+```
108108+109109+Add Deno to your shell path:
110110+111111+```sh
112112+echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
113113+source ~/.bashrc
114114+```
115115+116116+### Fedora / RHEL
117117+118118+Install the build toolchain and Git:
119119+120120+```sh
121121+sudo dnf install -y clang lld make git curl
122122+```
123123+124124+Install Deno with the official installer:
125125+126126+```sh
127127+curl -fsSL https://deno.land/install.sh | sh
128128+```
129129+130130+Add Deno to your shell path:
131131+132132+```sh
133133+echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
134134+source ~/.bashrc
135135+```
136136+137137+### Windows
138138+139139+The recommended Windows build-host path is **WSL2 with Ubuntu**.
140140+141141+From an elevated PowerShell prompt:
142142+143143+```powershell
144144+wsl --install -d Ubuntu
145145+```
146146+147147+Then open the Ubuntu environment and follow the **Debian / Ubuntu** instructions
148148+above.
149149+150150+The build host runs inside WSL2, but the resulting WebAssembly artifacts are
151151+intended to run on **native Windows** at runtime.
152152+153153+## Verify the toolchain
154154+155155+Before building, confirm the required tools are available:
156156+157157+```sh
158158+git --version
159159+make --version
160160+clang --version
161161+deno --version
162162+```
163163+164164+For a quick wasm-target smoke test, make sure `clang` can compile for `wasm32`:
165165+166166+```sh
167167+clang --target=wasm32 -c -x c /dev/null -o /tmp/clayterm-wasm-test.o
168168+rm -f /tmp/clayterm-wasm-test.o
169169+```
170170+171171+On macOS, if `which clang` still points to `/usr/bin/clang` and the wasm test
172172+fails, make sure the Homebrew LLVM `bin/` directory is at the front of your
173173+`PATH`.
174174+175175+## Build from source
176176+177177+Run the local source build from the repository root:
178178+179179+```sh
180180+make
181181+```
182182+183183+This should produce:
184184+185185+- `clayterm.wasm`
186186+- `wasm.ts`
187187+188188+For a clean rebuild:
189189+190190+```sh
191191+make clean && make
192192+```
193193+194194+## When to rebuild
195195+196196+Re-run `make` when:
197197+198198+- you change files under `src/`
199199+- you update the `clay` submodule
200200+- `clayterm.wasm` or `wasm.ts` is missing
201201+- generated outputs look stale after switching branches or pulling changes
202202+203203+When in doubt, use a clean rebuild:
204204+205205+```sh
206206+make clean && make
207207+```
208208+209209+## Verify the build
210210+211211+After `make` succeeds, run the test suite:
212212+213213+```sh
214214+deno task test
215215+```
216216+217217+Before opening a PR, it is also a good idea to run the same checks CI runs:
218218+219219+```sh
220220+deno task fmt:check
221221+deno lint
222222+```
223223+224224+## Troubleshooting
225225+226226+### `clay/` is missing or empty
227227+228228+Symptoms may include build failures such as:
229229+230230+- `fatal error: '../clay/clay.h' file not found`
231231+232232+Recovery:
233233+234234+```sh
235235+git submodule update --init --recursive
236236+```
237237+238238+Then verify the submodule state and rebuild:
239239+240240+```sh
241241+git submodule status --recursive
242242+make clean && make
243243+```
244244+245245+### `clang` cannot target `wasm32`
246246+247247+Symptoms may include:
248248+249249+- target-related `clang` errors mentioning `wasm32`
250250+- linker failures while producing `clayterm.wasm`
251251+252252+Recovery:
253253+254254+- make sure you are using an LLVM/Clang build with wasm support
255255+- on macOS, prefer the Homebrew `llvm` toolchain over `/usr/bin/clang`
256256+- on Linux/WSL2, make sure both `clang` and `lld` are installed
257257+- rerun the wasm smoke test:
258258+259259+```sh
260260+clang --target=wasm32 -c -x c /dev/null -o /tmp/clayterm-wasm-test.o
261261+rm -f /tmp/clayterm-wasm-test.o
262262+```
263263+264264+If the smoke test fails, fix the toolchain first and only then rerun `make`.
265265+266266+### Generated artifacts are missing or stale
267267+268268+Symptoms may include:
269269+270270+- `clayterm.wasm` is missing
271271+- `wasm.ts` is missing
272272+- you changed `src/` or updated `clay/`, but the generated outputs do not match
273273+274274+Recovery:
275275+276276+```sh
277277+make clean && make
278278+```
279279+280280+Then verify the repo is in a good state:
281281+282282+```sh
283283+deno task test
284284+```
285285+286286+## Scope note
287287+288288+This document is intentionally limited to local source builds for development.
289289+290290+Out of scope:
291291+292292+- `deno task build:npm`
293293+- `deno task build:jsr`
294294+- `npm publish`
295295+- `deno publish`
296296+- release tagging and package publishing workflows
···2828 percent,
2929 rgba,
3030 text,
3131-} from "../mod.ts";
3232-import { alternateBuffer, cursor, settings } from "../settings.ts";
3131+} from "../../mod.ts";
3232+import { alternateBuffer, cursor, settings } from "../../settings.ts";
3333import { useInput } from "./use-input.ts";
3434import { useStdin } from "./use-stdin.ts";
3535
+1-1
demo/use-input.ts
examples/keyboard/use-input.ts
···1111 suspend,
1212 until,
1313} from "effection";
1414-import { createInput, type InputEvent, type InputOptions } from "../mod.ts";
1414+import { createInput, type InputEvent, type InputOptions } from "../../mod.ts";
15151616function nothing() {
1717 return suspend() as unknown as Operation<
···11+# examples
22+33+This directory contains runnable example applications that exercise different
44+features of libs. If any of these examples are not working, please open an issue
55+with information about your terminal, shell, operating system and any other
66+information that could be pertinent to reproducing the issue.
77+88+> [!NOTE]
99+> Run the commands in this document from the repository root.
1010+1111+## Prerequisites
1212+1313+Build the generated WebAssembly bundle before running the examples:
1414+1515+```sh
1616+make
1717+```
1818+1919+## Keyboard
2020+2121+Path: `examples/keyboard/index.ts`
2222+2323+Run it with:
2424+2525+```sh
2626+deno run examples/keyboard/index.ts
2727+```
2828+2929+What it shows:
3030+3131+- raw keyboard input decoded into structured key events
3232+- progressive keyboard protocol support
3333+- pointer tracking and hover/click-driven UI updates
3434+- terminal mode configuration such as alternate buffer, hidden cursor, and mouse
3535+ reporting
3636+3737+Related files:
3838+3939+- `examples/keyboard/use-input.ts` wraps the input parser as a stream of decoded
4040+ events
4141+- `examples/keyboard/use-stdin.ts` adapts stdin into a byte stream for the demo
4242+4343+## Inline Regions
4444+4545+Path: `examples/inline-regions/index.ts`
4646+4747+Run it with:
4848+4949+```sh
5050+deno run examples/inline-regions/index.ts
5151+```
5252+5353+What it shows:
5454+5555+- rendering animated regions into normal terminal scrollback
5656+- querying cursor position with DSR to place later frames correctly
5757+- updating a previously allocated region without taking over the whole screen
5858+- small animated demos including a spinner, a progress bar, and a nyan-cat-style
5959+ sequence
6060+6161+This example is useful if you want to embed transient or animated UI output into
6262+a normal command-line workflow instead of switching to a full-screen alternate
6363+buffer interface.
+72-5
ops.ts
···55const OP_OPEN_ELEMENT = 0x02;
66const OP_TEXT = 0x03;
77const OP_CLOSE_ELEMENT = 0x04;
88+const OP_SNAPSHOT = 0x05;
89910/* Property group masks for OPEN_ELEMENT */
1011const PROP_LAYOUT = 0x01;
···5657 return o;
5758}
58595959-function packString(view: DataView, bytes: Uint8Array, o: number): number {
6060+function packString(
6161+ view: DataView,
6262+ bytes: Uint8Array,
6363+ o: number,
6464+ end: number,
6565+ context: string,
6666+): number {
6767+ let paddedLength = Math.ceil(bytes.length / 4) * 4;
6868+ let next = o + 4 + paddedLength;
6969+ if (next > end) {
7070+ throw new RangeError(
7171+ `clayterm transfer buffer capacity exceeded while packing ${context} ` +
7272+ `(${next} byte offset, ${end} byte limit). ` +
7373+ `Render a smaller visible slice or reduce frame content.`,
7474+ );
7575+ }
7676+6077 view.setUint32(o, bytes.length, true);
6178 o += 4;
6279 new Uint8Array(view.buffer).set(bytes, o);
6363- o += Math.ceil(bytes.length / 4) * 4;
8080+ o += paddedLength;
6481 return o;
6582}
6683···86103 o += 4;
8710488105 let bytes = encoder.encode(op.id);
8989- o = packString(view, bytes, o);
106106+ o = packString(view, bytes, o, end, "element id");
9010791108 let mask = 0;
92109 if (op.layout) mask |= PROP_LAYOUT;
···196213 break;
197214 }
198215216216+ case OP_SNAPSHOT: {
217217+ new Uint8Array(mem).set(op.data, o);
218218+ o += op.data.length;
219219+ break;
220220+ }
221221+199222 case OP_TEXT: {
200223 view.setUint32(o, OP_TEXT, true);
201224 o += 4;
···212235 o += 4;
213236214237 let str = encoder.encode(op.content);
215215- o = packString(view, str, o);
238238+ o = packString(view, str, o, end, "text content");
216239 break;
217240 }
218241 }
···302325 attrs?: number;
303326}
304327305305-export type Op = OpenElement | Text | CloseElement;
328328+interface Snapshot {
329329+ directive: typeof OP_SNAPSHOT;
330330+ data: Uint8Array;
331331+}
332332+333333+export type Op = OpenElement | Text | CloseElement | Snapshot;
306334307335export function open(
308336 id: string,
···321349export function close(): CloseElement {
322350 return { directive: OP_CLOSE_ELEMENT };
323351}
352352+353353+function packSize(ops: Op[]): number {
354354+ let n = 0;
355355+ for (let op of ops) {
356356+ switch (op.directive) {
357357+ case OP_CLOSE_ELEMENT:
358358+ n += 4;
359359+ break;
360360+ case OP_SNAPSHOT:
361361+ n += op.data.length;
362362+ break;
363363+ case OP_OPEN_ELEMENT: {
364364+ n += 4; // opcode
365365+ n += 4 + Math.ceil(encoder.encode(op.id).length / 4) * 4; // id string
366366+ n += 4; // mask
367367+ if (op.layout) n += 6 * 4 + 4 + 4 + 4; // 2 axes (3 words each) + pad + gap + align
368368+ if (op.bg !== undefined) n += 4;
369369+ if (op.cornerRadius) n += 4;
370370+ if (op.border) n += 8;
371371+ if (op.clip) n += 4;
372372+ if (op.floating) n += 16;
373373+ break;
374374+ }
375375+ case OP_TEXT: {
376376+ n += 4 + 4 + 4; // opcode + color + cfg
377377+ n += 4 + Math.ceil(encoder.encode(op.content).length / 4) * 4; // string
378378+ break;
379379+ }
380380+ }
381381+ }
382382+ return n;
383383+}
384384+385385+export function snapshot(ops: Op[]): Op {
386386+ let size = packSize(ops);
387387+ let buf = new ArrayBuffer(size);
388388+ let words = pack(ops, buf, 0, size);
389389+ return { directive: OP_SNAPSHOT, data: new Uint8Array(buf, 0, words * 4) };
390390+}
···406406The set of styling properties accepted by `props` is part of the current
407407implementation surface and may be extended.
408408409409+#### 8.3.4 snapshot
410410+411411+```
412412+snapshot(ops: Op[]): Op
413413+```
414414+415415+Creates a snapshot by pre-packing the given directive array into its transfer
416416+encoding. The returned value is an `Op` and can appear anywhere in a directive
417417+array where the original ops would have appeared. The internal representation is
418418+opaque.
419419+420420+When the renderer encounters a snapshot during transfer, it copies the
421421+pre-packed bytes directly into the command buffer without re-encoding. The
422422+snapshot's ops MUST be structurally balanced (every `open` matched by a
423423+`close`).
424424+425425+Snapshots enable higher-level frameworks to implement dirty tracking: a
426426+component whose inputs have not changed can reuse a previously created snapshot,
427427+avoiding the cost of re-packing its subtree each frame.
428428+409429### 8.4 Sizing helpers
410430411431These functions produce sizing-axis values for use in element layout
···462482directive arrays before rendering. The renderer's behavior when given an invalid
463483directive array is unspecified by this specification.
464484485485+A snapshot is semantically equivalent to splicing its source ops into the array
486486+at the snapshot's position. The renderer MUST produce identical layout and
487487+output regardless of whether ops are provided directly or via a snapshot.
488488+465489### 9.2 Transfer to the WASM module
466490467491As part of the render transaction, the directive array is transferred into a
468492form that the WASM module can process. This transfer is handled internally by
469493the renderer and is not an operation the caller performs or observes. The
470494transfer mechanism is an implementation detail described in Section 12.1.
495495+496496+If a frame exceeds transfer-buffer capacity while packing string content, the
497497+renderer MUST throw a descriptive `RangeError` that identifies the condition as
498498+a transfer-buffer, frame-capacity, or packing overflow. The renderer MUST NOT
499499+expose only the raw host-level TypedArray message `"offset is out of bounds"`
500500+for this condition. The error message SHOULD direct callers to render a smaller
501501+visible slice or reduce frame content.
471502472503### 9.3 Directive identity
473504