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
|
# DOM web backend
The web backend keeps the existing Pardes core and replaces the old
Emscripten/SDL/WebGL shell. Zig produces a `wasm32-freestanding` module whose
only imports are the four browser capabilities listed below — no libc, no WASI,
no runtime. JavaScript owns `requestAnimationFrame`, browser input and window
sizing; the CORE owns the loop and JavaScript calls one iteration of it per
frame. HTML and CSS render selectable, accessible text. There is no canvas.
The default web build links Tree-sitter's C runtime and the Zig grammar inside
that same import-free module. A small freestanding compatibility layer supplies
the C ABI locally, so source panes retain the core's Tree-sitter highlighting
without Emscripten, WASI, or browser libc imports. Other grammar tiers remain
selectable with `-Dtree-sitter=minimal` or `-Dtree-sitter=full`.
`src/web.zig` is the narrow WASM ABI. Events enter through exported functions
that hand them straight to `Pardes.update`. `pardes_tick` is one
`Pardes.pump` — queued input, effects, render, present — and the host's
`present` packs the canonical `Surface` into a 20-byte cell array that
`pardes_frame`/`_ptr`/`_cols`/`_rows` describe. `src/web/app.mjs` reads that
array in one linear pass and patches stable DOM nodes.
Animated time is JavaScript's alone. `pardes_animation_tick` spends one fixed
60 Hz step and is the only entry that advances a transition, exactly as the
AppKit shell keeps its display clock apart from its pumps: if every input pump
also advanced a fade, a burst of keys would collapse ten frames into one.
`pardes_tick` never advances animated time itself, and no longer has to undo
anything to avoid it: `Pardes.pump` does not post a tick for its caller, since
only the host knows when a real frame interval has passed.
Cell attribute bit 7 carries the core's tagline font role. The DOM keeps every
cell at the body grid's fixed width and height, but scales and centres the
tagline glyph using the build-time `gui_tagline_font_percent`; the percentage is
exported by the module rather than duplicated in JavaScript. Animation time is
also host-independent: `requestAnimationFrame` time is accumulated into 60 Hz
core ticks, so 120/144 Hz displays do not accelerate frame-count transitions
and a returning background tab has bounded catch-up work.
The `web` platform is deliberately not one of the shader-capable native GUI
shells, and two comptime capability flags say so once each. It exposes no
`Panel*` transition builtins (`PanelSlide`, `PanelZoom`, `PanelDissolve`,
`PanelAscii`, `PanelVertical`, `PanelEdges`, `PanelFall`, `PanelWave`,
`PanelCurtain`, `PanelScramble`, `PanelType`) because
`capabilities.panel_transitions` is `pardes.hosted`, and no
`Crt`/`Ripple`/`Glitch` because `capabilities.scene_shaders` is
`platform == .gui or platform == .macos` (`src/builtins.zig:41-42`; the words
themselves carry those availabilities in `src/runtime_config.zig:155-168`).
Applying either faithfully would require a second canvas renderer and give up
the DOM renderer's selectable/accessibility contract. Theme fades and the
delayed, side-effect-free Look hover remain grid animations and continue to use
the fixed 60 Hz ticks above.
Build a replay from a dump:
```sh
zig build web \
-Dplatform=web \
-Dtarget=wasm32-freestanding \
-Dtree-sitter=zig \
-Ddump=test/web-snapshots/source-list.dump.zon
```
The output is in `zig-out/web`. Serve that directory over HTTP; browsers do not
allow a useful WASM module load from `file:` URLs.
The browser shell has no argv: every command-line flag `src/main.zig` parses —
`--tty`, `--tty-toggle`, `-n`, `-l`, `--fs[=<dir>]`, `--nested`,
`--detach[=<name>]`, `--attach[=<name>]`, `-h`/`--help`, `--version`, and the
positional file-or-directory — is native-only, because the wasm module roots at
`src/web.zig` and never links `main.zig`. State comes from the embedded dump
instead, and there is nothing here to answer `--version` with even though
`pardes.version` and `pardes.commit` are compiled in like everywhere else.
Web LOOK's source archive follows Git's working-tree view: tracked `.zig` files
plus new, nonignored ones, sorted by path. The checked-in browser replay is a
real native TTY dump of that same selection; its top and bottom launcher
snapshots jointly cover the complete list before opening `build.zig`.
What is genuinely absent is narrower than "no IO". The module has no threads
and no host filesystem, so there is no startup config directory, init file,
ThemeFile, or DumpThemes; LOOK resolves
against the build-generated source archive rather than disk, and no language
BACKEND is compiled in (`zls_backend` is off for wasm — which also means
`lsp.supports` is empty, so the core never even raises a language query there,
and the host leaves `lsp` null).
Replay terminals retain the same core shape but use Zig's failing IO value;
this keeps the module freestanding without instantiating POSIX threaded IO that
the browser can never call.
`Attach` and `Detach` are absent for the same structural reason and behind the
same gate: both are `enabled = pardes.hosted` (`src/builtins.zig`, and
`pardes.hosted` is `tty or gui or macos`), because a detached session is a unix
socket and a page has none. Nothing in this shell speaks
`src/detached/wire.zig`, and `push_detach` is one of the null vtable methods
below.
`src/web.zig` fills in six methods of `Host.VTable` and leaves the rest null,
and a null method is answered by the core itself rather than forwarded to a
page that could not honour it. The six are `present`, `write_file`,
`write_dump`, `set_clipboard`, `read_clipboard` and `open_link`; the last five
are backed by four imports from the module `pardes` that JavaScript supplies at
instantiation, because a written file and a written dump are both a download:
| import | effect behind it |
|---|---|
| `host_open_link(url, len)` | `open_link` — `window.open` |
| `host_set_clipboard(text, len)` | `set_clipboard` — `navigator.clipboard.writeText` |
| `host_read_clipboard()` | `read_clipboard` — answered later, or never, by `pardes_paste` |
| `host_download(path, path_len, bytes, len)` | `save_file`, `save_text` and `write_dump`: a file a page writes is a download |
What the core answers instead of asking: `spawn`, `pty_write` and `pty_resize`
land in the per-instance fallback, so a pane with no child is SILENT rather
than synthetic; `tty_taken` is never taken; any save the page declines resolves
against the core's virtual filesystem, which is also where every read comes
from — the core reads files itself and never asks a host; `watch_file` records
the request; `lsp` answers an empty row set at once (`zls_backend` is off for
wasm, so the core never raises a query there anyway) and `pipe` answers
failure; `wait_input` is null because wasm must never block, and `watch_theme`
and `dump_themes` are not reachable without a config directory. Of the
remaining nulls, `post_present` and `poll_frame` are per-frame host bookkeeping
a page has none of, `detach` has no session to leave, `gpio_toggle` belongs to
the ESP32-P4 board, and `fs_reply` answers a FUSE control filesystem that
`--fs` is the only way to ask for. `quit` sets the
core's flag, which `pardes_should_quit` reports so the page can stop its frame
loop.
Two dependency-free test layers cover the boundary:
```sh
# Real WASM in Node plus a small DOM double.
zig build web-harness \
-Dplatform=web \
-Dtarget=wasm32-freestanding \
-Dtree-sitter=zig \
-Ddump=test/web-snapshots/source-list.dump.zon
# Real headless Chrome, DOM rendering, and browser touch input.
zig build web-e2e
```
Set `PARDES_CHROME` when Chrome/Chromium is not on `PATH` or in a normal
Playwright/Puppeteer cache.
|