From c3d0b84b7961ae26d2d654e7120821cc2d83d20d Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 24 Aug 2026 10:56:05 -0300 Subject: host: the core owns the event loop; every platform becomes a vtable of optional methods --- docs/design.typ | 2 +- docs/lsp.md | 6 +++--- docs/web.md | 67 +++++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 50 insertions(+), 25 deletions(-) (limited to 'docs') diff --git a/docs/design.typ b/docs/design.typ index 0d8f03dd..3e387f7a 100644 --- a/docs/design.typ +++ b/docs/design.typ @@ -104,7 +104,7 @@ serial and carry only phase, effect, frame, and from/to cell boxes. Canonical layout is committed immediately; tracks are finite presentation data.) *Effect* (out, queued): `spawn{pane, cwd}`, `write{pane, bytes}`, -`resize_pty{pane, cols, rows}`, `open_link`, `new_file{pane, serial}`, +`resize_pty{pane, cols, rows}`, `open_link`, `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 diff --git a/docs/lsp.md b/docs/lsp.md index 0125f958..54ffd764 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -33,9 +33,9 @@ about thirty lines per shell: `tty.zig` uses `io.concurrent` + the vaxis loop queue, `gui.zig` uses a detached thread + the mutex queue it already had. The web shell compiles in no backend at all (`zls_backend` is off for wasm), which makes `lsp.supports` empty, which makes `lspRequest` return before it emits — -so on the web the effect is never even raised. `web.zig` carries a prong for it -and exports `pardes_lsp_response` anyway; both are unreachable in the shipped -configuration and wait for a host that links a backend. +so on the web the effect is never even raised. `web.zig` leaves the host's +`lsp` method null and exports nothing for a response; a host that links a +backend would add both. Three rules make it safe: diff --git a/docs/web.md b/docs/web.md index 793c79bc..0f8c068f 100644 --- a/docs/web.md +++ b/docs/web.md @@ -1,10 +1,11 @@ # 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 with no -imports. Vanilla JavaScript owns `requestAnimationFrame`, browser input, window -sizing, and effect/IO dispatch; HTML and CSS render selectable, accessible text. -There is no canvas. +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 @@ -12,11 +13,20 @@ 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, -and `pardes_frame` writes the canonical `Surface` into a packed 20-byte cell -array. `src/web/app.mjs` reads that array in one linear pass and patches stable -DOM nodes. Core effects become browser operations (links, downloads, clipboard) -or `pardes-io` custom events for a process-capable embedding host. +`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 @@ -65,22 +75,37 @@ 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; -`web.zig`'s prong for it is waiting for a host that links one). +`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. -The EFFECTS themselves are all still emitted, each as a numbered code across -the ABI: `spawn` 1, `write` 2, `resize_pty` 3, `open_link` 4, `save_file` 5, -`write_dump` 6, `set_clipboard` 7, `lsp` 8, `watch` 9, `quit` 10, `new_file` -11, `pipe` 12, `read_clipboard` 13. Inbound, `pardes_output`, -`pardes_set_cwd`, `pardes_eof`, `pardes_paste` and `pardes_lsp_response` -exist to answer them. A plain page ignores most of those and gets a replay -viewer; a -process-capable host (the `pardes-io` events above) answers them and gets -terminals. The core does not know the difference — that is the point of the -seam. +`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 `theme_file` +and `dump_themes` are not reachable without a config directory. `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: -- cgit v1.3