1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
|
#set page(margin: 2.2cm)
#set text(font: "New Computer Modern", size: 10.5pt)
#set par(justify: true)
#show heading: set block(above: 1.4em, below: 0.8em)
#align(center)[
#text(17pt)[*Pardes: A Text Environment*]
#text(11pt)[design for the rewrite — 2026-07-05]
]
= What it is
Pardes is a text environment in the acme tradition: columns of panes, each pane a
tag line plus a body; the body is a live terminal, a file, an image, or a PDF. The
mouse carries meaning — left selects, middle executes, right looks. Everything on
screen is text and all text is equally alive, whether the shell printed it or the
user typed it.
It runs in four places: the terminal (libvaxis), a native SDL3 window (the
steamdeck), the browser (a freestanding wasm core driven by vanilla
JavaScript and rendered as HTML/CSS), and a native macOS app (an AppKit and
CoreText shell over a static `libpardes.a`). The rewrite exists because
the prototype grew three parallel implementations of one program. The rewrite has
exactly one program and four thin shells.
= The shape
Pardes is a *library*, in the way ghostty-vt is a library: you feed it bytes and
events, and you read state out of it. Every platform owns its own event loop and
its own renderer; the core owns everything the user would recognize as pardes.
#table(
columns: (auto, 1fr),
stroke: 0.4pt,
[*core*], [layout, panes, modes, selection, click semantics, themes, text of the
UI. Pure state machine: `update(event)` mutates, `render(arena)` returns the
surface. No rendering, no event loop, and no IO that has an effect for it —
but "no syscalls" was never true and is less true now: `look` reads a file
a Look opened, walks a directory for Find and reads every candidate for
Grep, `fonts` walks the font directories, and MuPDF opens a `.pdf`. Those
are the ones that are cheaper done in place than round-tripped through an
effect and back; everything with a lifetime — a pty, a window, the
clipboard — is still asked for.],
[*shell*], [one MODULE per platform (tty is one file; gui adds the CRT and
gamepad files, a C font loader and twelve GLSL shaders; web and macOS each
add a host language).
Owns the event loop, translates native input
into core events, renders the core's surface, and performs the core's
requested effects (spawn a shell, write a pty, open a link).],
)
This also collapses the prototype's *two* applications into one: the browser
build today is a separate 800-line read-only replay viewer. In the rewrite,
loading a dump of another instance is a first-class feature of the one
application, the way acme handles dumps: `pardes -l state.zon` on every
platform reconstructs the panes (terminals by replaying their raw VT streams
into fresh emulators, files, images and PDFs from their bytes — a PDF rides the
dump's `image` kind, carrying its path, its bytes and the page it was on). The web shell embeds
a dump and leaves `spawn` unanswered. Same core, no viewer fork.
The boundary is two data types, both plain values:
*Event* (in): `key` (which carries typed `text`), `mouse` (press/release/motion/
drag; button left, middle, right and the four wheel directions; cell position;
and the `ctrl` flag, because Ctrl-left-click is goto-definition), `pinch`,
`touch_scroll`, `resize` (cols, rows, and — in a MuPDF build — the cell's pixel
size), `paste`, `pdf_scroll`,
`output` (bytes a pty produced, tagged with the pane id), `eof`, `command` (a
builtin line arriving from another process over the nested socket), `tick`, and
the answers to an ask: `lsp_resp`, `pipe_resp`, `file_changed`. Touch
policy belongs to the shell: web maps
a one-finger tap to right-button LOOK and a drag past its tap slop to natural
scrolling; native SDL keeps its two-finger gestures. A joystick cursor is a
motion plus buttons. The shells do this translation and nothing else with
input: one event vocabulary, four dialects translated at the door.
*Surface* (out): `cols`, `rows`, a grid of cells — grapheme, fg, bg, attrs — a
cursor, and the pixel attachments. This is the *canonical interface*: it is literally what the
tty shell hands to vaxis, cell by cell. SDL rasterizes the same grid through a
glyph atlas; the browser reads a packed copy and patches native DOM cells. If it
cannot be expressed in the surface, it does not
exist in pardes. (Pixel images ride along as a list of PLACEMENTS rather than
one per pane — a PDF pane contributes every page its viewport intersects, and
pages can be arbitrarily short. The SDL shells blit RGBA, the tty shell falls
back to the petscii matcher, kitty
graphics when available. The per-pane rectangles live on `Pardes`, not on the
surface: a shell that wants them asks the core.)
*Effect* (out, queued): `spawn{pane, cwd}`, `write{pane, bytes}`,
`resize_pty{pane, cols, rows}`, `open_link`, `new_file{pane, serial}`,
`save_file{pane}`, `write_dump`, `set_clipboard`, `read_clipboard`, `lsp`,
`pipe`, `watch`, `quit`.
The core never performs IO for any of these; it asks — and four of the asks have an
answer coming back: `read_clipboard` returns an ordinary `paste` event (or
nothing at all when the shell cannot read the clipboard — most terminals refuse
the OSC 52 read), `lsp` returns `lsp_resp`, `pipe` returns `pipe_resp`, and
`watch` returns `file_changed` whenever the shell notices the file moved under
it. This
is what makes the browser build honest instead of a
fork: a wasm shell simply
answers `spawn` differently (or not at all) — the core does not know.
Platform divergence inside the core is a comptime tag, used the way the stdlib
uses `os.tag`:
```zig
pub const Platform = enum { tty, gui, web, macos };
// in look.zig:
if (platform == .web and target.kind == .url)
return .{ .open_link = target.text };
```
There is one such file by design: `look.zig` holds *what a click on text means* —
expansion of the word under the click (acme's `isfilec`), path/`:line`
resolution, url detection, and the per-platform outcomes, kept adjacent so the
divergence is visible in one screenful.
= Data structures
The whole state is one struct, fixed-size where it can be. Panes themselves are
heap-allocated on demand and their contents (file bytes, the yank register, PDF
rasters, tree-sitter state) grow with what you open; everything else is sized
at init.
```zig
Pardes
ncol + col_weight[6], col_terms[6][16], col_n[6] // columns as flat arrays
panes: [16]?*Pane // slot array; id = index
active: usize
drag: Drag // none | select | move | border_v | border_h | tag
theme_idx: usize // index into themes (228 of them)
colors_on: bool
rects: [16]Rect // where each pane landed, this frame
effects: [4096]Effect + head/len // a fixed ring
Pane
tag: TagLine // live prefix (cwd/path) + editable tail
kind: enum { terminal, file, image, pdf } // + independent optional payloads
vt, stream: ghostty-vt Terminal and its stream — on EVERY pane, not just
terminals, which is what lets the same keys and the
same parity suite drive a file and a shell
file: ?File / image: ?Image / pdf: ?PdfView
vweight: f32
mode: enum { normal, insert, tty } // helix-modal; tty = raw to the pty
// `v` adds a fourth thing to DISPLAY, "select",
// which is a bool on top of normal, not a mode
cursor: absolute body position // rides the scrollback, not the screen
sel: [3]Sel + msel/vsel + sels[63] // per-button block sels, the line and
// char modal ones, and up to 64 cursors
ovl: ?Ovl // ONE typed run, anchored to an absolute row
undo: two stacks, not one — ed_undo[64] of overlay snapshots for a terminal,
// and File.undo of content snapshots for a file
File = path + bytes + line index + Syn (tree-sitter highlight bytes)
Image = decoded RGBA + petscii grid cache
PdfView = MuPDF document + per-page rasters + cached outline
```
Layout is arithmetic, not objects: columns are weights over the width, panes are
weights over the column. `splitBelow` shrinks only the source pane;
`absorbVWeight` gives a dying pane's weight to one sibling; an emptied column
hands its width to a neighbor. Minimal motion is the invariant: an operation on
one pane may not move panes it does not touch.
= Style
TigerStyle, plus house rules proven in the prototype: imperative and flat; one
big `update` dispatch, not handler objects; no one-line helpers — inline the
four-line scan; assert invariants at entry (`assert(vsum > 0)`); static
allocation at init, arenas per frame.
The metric was lines of code, and for the rewrite itself it held: 7,626 lines
replaced the prototype's 12,072 with three backends instead of one and a half.
It has not held since, and the table below is the honest version rather than
the flattering one. Everything after the rewrite — PDF, a language backend, a
fourth shell, 228 themes, multiple cursors — was added, not traded for
something removed:
#table(
columns: (auto, auto, auto, 1fr),
stroke: 0.4pt,
[*module*], [*at the rewrite*], [*now*], [],
[core and the pieces that grew out of it — 25 files: pardes, look, syntax, image, dump, builtins, config, output/file/term panes, normal\_input, nested, fonts, themes, …], [4,289], [22,220], [all semantics, all platforms],
[modal.zig + petscii.zig (pure, unit-testable)], [1,410], [2,569], [carried over],
[pdf.zig], [—], [1,581], [MuPDF, `-Dmupdf`],
[lsp/ (seam + in-process ZLS backend)], [—], [1,663], [`.zig` only],
[shells: tty 1,396; gui 4,968; web 601; macos.zig 1,579], [2,768], [8,544], [translate + render only],
[main.zig + build.zig], [484], [1,471], [four targets, four codegen passes],
[*application total* — every `.zig` under `src/`, plus `build.zig`], [*7,626*], [*38,048*], [vs the prototype's 12,072],
)
Those six rows are a partition: no file is in two of them and none is left out,
which the old table could not say (its rows summed to 1,325 more than its own
total, and twenty-six files were in no row at all). What the total deliberately
does NOT count, because none of it is Zig under `src/`: 2,250 lines of Swift for
the macOS app and its icon generator, 1,541 lines of C bridging MuPDF, 549 of
JavaScript, HTML and CSS for the web shell, 216 of C for the SDL font loader,
`mupdf.zig` (355) at the root, and `tools/` + `build/` (896). The test tree is
another 5,234 lines of Zig across eight harnesses, plus 1,028 of Swift and 610
of JavaScript — against 831 at the rewrite.
Two things that number is not. It is not one program's worth of growth — the
macOS and web shells and the PDF and language work are four products sharing a
core. And it is not licence: the rule that survives is the local one, that a
change should leave the file it touches no longer than it found it.
= Testing: the old program is the oracle
Before the rewrite compiled, the prototype got a harness (`snapshot.zig`, the
`zig build snap` step): it forks either binary in a pty, feeds it an *event
script* (one line per input: keys, SGR mouse, resizes, sync points), and
captures the rendered grid — text, cursor, and per-cell style runs — through
its own ghostty terminal. Goldens are generated from the old binary
(`zig build snap -- --update`); the new binary must reproduce them byte for
byte (`pardes-snap pardes/zig-out/bin/pardes`). Eighteen scripts covered the
checklist in Appendix A at the rewrite; eighty-seven cover it and everything
since. All pass. Determinism pins: fixed workdir paths (they
appear in tags), a controlled `$HOME` with `PS1='$ '`, `LC_ALL=C`, and a
grid-stability sync primitive instead of timing guesses.
Three deliberate deviations surfaced by the oracle, kept after review: the
greeting `ls` waits for the shell's first output (the prototype raced bash's
startup and won only by allocator luck); dump files compare with base64 pty
history elided (it encodes prompt-redraw micro-timing, not state — the cleaned
text fields are the contract); and typed insert runs don't survive a dump
replay (they are an overlay, not pty bytes — the prototype's replay viewer had
the same semantics).
Shell correctness is no longer out of scope, and that is the biggest change to
this section since the rewrite. `web-snap` / `web-e2e` drive headless Chromium
over CDP with real DOM pointer and touch events and diff
`test/web-snapshots/`; `image-harness` and `pdf-harness` snapshot native PIXEL
output through kitty graphics and SDL; `macos-e2e` is an offscreen AppKit
snapshot suite over its own seven scripts. What remains untested by a snapshot
is the last hop — that vaxis diffs correctly onto a real terminal — plus each
shell's inline unit tests (the FreeType atlas raster, the trackpad and rotation
maths, the gamepad replay).
= Build
Every `zig build` builds ONE platform: `platform` is an option defaulting to
`.tty`, so a bare `zig build` gives `pardes` (vaxis) and the other three are
separate invocations — `-Dplatform=gui` for `pardes-gui` (SDL3),
`-Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>` for
`pardes.wasm` plus a vanilla DOM shell (any other spelling of that one fails on
purpose rather than building something silently wrong), and `-Dplatform=macos`
plus the `macos-app` step for `pardes.app` wrapped around a static
`libpardes.a` on a Darwin host. Native dependencies are ghostty, vaxis, uucode (shared config),
zstbi, SDL (a pinned fork, lazy), FreeType, MuPDF (`-Dmupdf`, on by default
everywhere but the web, lazy), ZLS, mvzr (the regex engine behind `s`/`S`), and
zig-tree-sitter + 26 grammars — all pinned
through `zig fetch` and wired in `build.zig`. Generated during the build:
`highlights.scm` → an options module; the vendored helix/zed theme
sources → the generated half of the theme ring; the tracked `.zig` sources →
the web shell's read-only archive; eight GLSL shaders → SPIR-V. That last one is
the only build input wanting a tool a stock machine lacks (`glslc`), so it is
also the only one whose output is committed: `zig build shaders` writes
`shaders/prebuilt/*.spv` and `-Dprebuilt-shaders` embeds that copy rather than
shelling out, which is what lets a gui build need nothing but a C toolchain. The
tutor and the embedded font are plain `@embedFile`s, not codegen. Debug builds are
incremental for the seconds-loop; release builds are the product.
= What is deliberately absent
No render abstraction over the shells (the surface *is* the abstraction). No
plugin system. No async runtime in the core — the shells may
thread, the core is single-threaded by construction. No 9P yet — but the
library boundary is exactly where acme put the file server, and a fifth shell
could serve `Surface` and `Event` over 9P without touching the core. Half of
that is already built and unnamed: `src/nested.zig` listens on a unix socket at
`$XDG_RUNTIME_DIR/pardes-<pid>.sock` (`~/.local/state/pardes` without one),
accepting exactly one verb — `Look <path>`, and nothing else, which is the
security property — and that is how
a `pardes <file>` run inside a pardes hands the file to the outer session
instead of stacking a second full-screen UI inside one of its panes.
"No config files" held until the startup file (`docs/config.md`) arrived, and
that is the narrowest thing the phrase could still cover: a list of builtin
COMMANDS run before the first frame — no schema, no new vocabulary, and no key
remapping. The keymap is `src/config.zig`, compiled in, where a wrong binding
is a compile error rather than a silent no-op.
= Appendix A: feature parity checklist
From the prototype survey; every line is covered by at least one of the
event scripts in `test/snapshots/` — the original eighteen (boot, tty, edit,
scroll, modal, look-file, look-dir, exec, tag, theme, tutor, windowops, dump,
load, ttyonly, syntax, fileedit, images), and sixty-nine more added since for
everything below that the prototype never had.
*Layout.* Columns by weight (≤6), panes by vweight (16 panes in total, not
per column); global topbar;
per-pane gutter (move box + scrollbar) and tag row; `splitBelow` shrinks only
the source (cursor row kept visible); dying pane's weight absorbed by one
sibling; emptied column hands width to a neighbor; border-drag resize on a
pane's own trailing edge (v and h), hover shows `╎`/`╌` glyph-only hints;
move-drag via the gutter box with preview; Alt-n new shell below, Alt-c move
pane to new column; Ctrl-w h/j/k/l directional focus; body-normal Esc hops to
the pane you were in before this one, alternating between two (the same
builtin as SPC j j);
`--tty` single-pane mode.
*Mouse.* Left: select (block, stays highlighted after drag, pins cursor,
enters normal), click clears, tag-row click enters tag edit, scrollbar
click scrolls up-to-row (right button: down), border/move drags. Middle:
execute — no-drag expands to file-ish word (alnum `.-+/:@_~`); builtin or send
to shell; does not focus. Right: look — peel `:NNN`, resolve against the
clicked pane's dir (`/proc/pid/cwd`, or a file's dirname) and, if that fails,
against every other live pane's, most-recently-focused first (the jump stack
backwards, duplicate dirs skipped; absolute words try one dir and stop) so a
relative name is openable from any window that can see it; dir → shell + `ls`
in source column (dedup by cwd), file → file pane at line (dedup by path,
first doc opens left column and may evict a lone pristine shell; later docs
split below the existing doc — an output pane, `+Search`/`+Help`, is not a
doc for either half of that rule: it never claims a column, it splits below
whatever pane asked for it, and nothing splits from it), image ext → image
pane, `.pdf` → PDF pane. Ctrl+left is goto-definition, the one chord borrowed
from every editor with a language backend. Two spellings the word expansion
has beyond a path: `` @`ls -la` `` is taken WHOLE and runs as a command rather
than opening as a file, and `@p7:10:5` addresses a live pane by number for the
things — terminals, output buffers — that have no path to name.
Middle+left chord: kept
left selection appended as trailing CLI argument. Wheel: scroll hovered pane,
batched.
*Modes.* normal: helix motions (`h j k l w b e W B E 0 $ ^`, `f F t T` and
`Alt-.`, counts, `gg ge gh gs gl g| G`, `Ctrl-d/u/f/b`, `zt zz zb zj zk`),
insert entries (`i a I A o O`), selections (`v` extend — displayed as a fourth
mode name, "select" — `x`/`X`/`Alt-x`, `%`, `;`/`Alt-;`, `_`), MULTIPLE CURSORS
up to 64 (`C`/`Alt-C` copy, `s`/`S` select and split by regex with a live
preview, `Alt-s` split on newline, `,`/`Alt-,` keep and remove the primary,
`)`/`(` rotate, `Alt--`/`Alt-_` merge), operators (`d c y p P R u U`, `Alt-d`
delete-noyank, `J`, `>`/`<`, `~`/`` ` ``/`` Alt-` ``, `Ctrl-a`/`Ctrl-x`,
`Ctrl-c` comment-toggle), textobjects and surrounds under `m`
(`mm mi ma ms mr md`), the `]`/`[` pairs (`]p ]d ]D ]<space>`), `/` with `n`/`N`,
`|` to filter the selection through a command, `:` for the tag as a command
line, and Enter=look Tab=execute at cursor. Since the helix motion model
landed, a traversal motion SELECTS the range it crossed — which is why there is
no verb+noun grammar and why `i` after `w` types at the selection's start. insert:
click-and-type; terminals get splice runs (shift right, never overwrite;
absolute-row anchored), files get real edits. tty: raw pty forwarding
(Ctrl-key toggle, default Ctrl-b, `--tty-toggle`), mouse still usable,
promptClickMove on entry, prompts visible (hidden in the other modes via
OSC 133). `y` fills the yank register and `p` pastes it; neither touches the
system clipboard, which is helix's five words — `SPC y/Y/p/P/R`, out via
`set_clipboard` and back via `read_clipboard` — and nothing else, so a delete
cannot clobber what the desktop was holding. A paste from an outer terminal
arrives bracketed, as one `paste` event.
*Tag.* Live prefix (mode indicator, cwd or path) + editable tail with the
full modal editor; defaults `New Del` / `Save New Del`; an image tag is
`img <path> New Del` like any other (its renderer toggles are builtins under
`SPC t p/l/a`). Topbar:
`New Newcol Find Grep Help Tutor Dump NextColor Debug Kill` — execute-only
(left click inert);
Colors and Crt left it for their leader paths.
*Panes.* Terminal: ghostty-vt, 16 MiB scrollback, OSC 133 prompt semantics,
OSC 7 cwd, DSR/DA/kitty-query replies (write_pty + device_attributes — the
nushell/helix regression), greeting `ls`, auto-follow output unless
navigating. File: line-number gutter (fixed width), tree-sitter highlights
(c/cpp/zig minimal tier; 26 grammars full tier; re-highlight on edit,
visible-range first), Save, open-at-line, undo/redo. Image: zstbi decode,
kitty graphics when available, petscii matcher fallback (C64/terminal
palettes, ascii glyph set toggle). PDF (`-Dmupdf`, native default on): MuPDF
rendering as one continuous page strip, real text search, mouse text
selection, the document outline into `+PdfSections`, fit-width/fit-height and
a themed duotone tint — the last three named in the pane's own live tag.
Tutor: embedded text as file pane.
*Language.* ZLS compiled in and called IN-PROCESS — no subprocess, no
JSON-RPC, no daemon and nothing cached between queries — behind a
one-function seam (`lsp.query`) so that swapping a backend touches nothing
else. `.zig` only, and on demand only: helix's five `g` gotos, ten builtins
under `SPC l`, `]d`/`[d`, `=`, and insert-mode Tab after a `.`. Answers become
rows in `+Search`, `+Hover` or `+Lsp` — output buffers of the same kind `/`,
Find and Help fill — or, for a rename, byte ranges the core applies in one undo. The
web shell has no threads and therefore no backend at all.
*Chrome.* One theme per `.zig` file, folded into the ring at comptime: ours
first — helix (default; near-black page, chrome one grey-ramp step off it,
colors lifted from the helix editor's own theme), dark, and the acme-light one
named simply `acme` — then
everything `tools/gen_themes.zig` exports at build time out of the vendored
helix `.toml` and zed `.json` sources in `vendor/themes`, which is all 214 helix
ships plus zed's 11, sorted by name. Names are unique by construction: helix's
own acme is vendored as `acme_helix.toml` so it cannot collide with ours, and
every zed theme takes a `_zed` suffix for the same reason. helix and dark leave
a child's ANSI palette native; acme and the zed exports resolve it onto
the page so shell output stays readable on a light one. A theme owns the page,
the tag bar, the gutter, the move box and the SELECTION: `sel_bg`/`sel_fg` are
one pair per theme, and the three per-button tints and the dimmed extra cursors
are mixed off it, so what stays fixed is the distinction between buttons and not
the colours. `NextColor` browses the ring one
step at a time — at 228 it is no longer how you REACH one —
`Theme <name>` jumps to one and `ThemeSel` (`SPC t t`) lists them all into an
output buffer whose rows are those very commands — execute a row (Tab, middle
click) and the theme goes on; n/N select such a row WHOLE, since a command
line holds no place to pick out of it — the third grain of that motion, the
other two being one stop per ROW in a results list (the location at its head,
never the matched text after it) and every look-able word in free text.
Colors toggles
all recolor passes; Debug stats overlay; Dump writes state ZON.
*Web (replay viewer parity).* Embedded dump; focus, border/move drags, wheel,
scrollbar, Colors/NextColor, and link-LOOK opening a new
tab (new — the prototype has no web link handling). One-finger touch is
deliberately complete by itself: tap = LOOK; drag past a small slop = natural
scroll, with no LOOK on release. A build-generated read-only archive lets LOOK
open the current contents of Git-tracked Pardes `.zig` files despite the web
shell having no host filesystem. The published launcher dump is captured from
a running Pardes TTY after `git ls-files '*.zig'`, so its complete terminal
listing is the set the user can open. The freestanding module carries no host
libc, SDL, or WebGL; Tree-sitter's C runtime and the selected parsers link into
the module through a tiny local ABI shim (Zig is the compact web default). A body gesture retains
tap-LOOK/drag-scroll, but finger-down on
a tagline or a one-cell-tolerant pane separator latches to a left-mouse gesture
for its lifetime, keeping layout drags out of the scroll heuristic. Touch
input translation and scroll/tap state live entirely in the JavaScript shell.
The native SDL shell uses a FreeType light-hinted grayscale atlas over the SDL GPU API (SPIR-V).
The browser exposes each cell as selectable, inspectable text and applies the
surface styles with CSS. The browser `.snap` harness drives real Chromium touch
input and reads both text and per-cell styles from the DOM renderer's packed
surface. Joystick cursor for the steamdeck is *new* scope — the prototype ships
no gamepad input.
|