From de4def3548a6729b0dfd2120495a61beef8c8c2c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 5 Jul 2026 20:21:50 -0300 Subject: pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options). --- docs/design.typ | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 docs/design.typ (limited to 'docs/design.typ') diff --git a/docs/design.typ b/docs/design.typ new file mode 100644 index 00000000..382063cd --- /dev/null +++ b/docs/design.typ @@ -0,0 +1,246 @@ +#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, or an image. 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 three places: the terminal (libvaxis), a native SDL3 window (the +steamdeck), and the browser (wasm + SDL3 + emscripten). The rewrite exists because +the prototype grew three parallel implementations of one program. The rewrite has +exactly one program and three 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, `surface()` describes. No + syscalls, no rendering, no event loop.], + [*shell*], [one file per platform. 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 and images from their bytes). 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`, `text`, `mouse` (press/release/motion; button left, middle, +right; cell position), `wheel`, `pinch`, `resize`, and `output` (bytes a pty +produced, tagged with the pane id). A touch tap is a left press; a joystick +cursor is a motion plus buttons; the shells do this translation and nothing else +with input. One event vocabulary, three dialects translated at the door. + +*Surface* (out): a grid of cells — grapheme, fg, bg, attrs — plus a cursor and +per-pane rectangles. This is the *canonical interface*: it is literally what the +tty shell hands to vaxis, cell by cell. The SDL shells rasterize the same grid +through a glyph atlas. If it cannot be expressed in the surface, it does not +exist in pardes. (Pixel images ride along as an attachment per pane: the SDL +shells blit RGBA, the tty shell falls back to the petscii matcher, kitty +graphics when available.) + +*Effect* (out, queued): `spawn{pane, cwd}`, `write{pane, bytes}`, +`resize_pty{pane, cols, rows}`, `open_link{url}`, `read_file{path}`, +`set_clipboard`, `bell`, `quit`. The core never performs IO; it asks. 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 }; +// 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, sized at init, no hidden allocation after: + +```zig +Pardes + cols: []Col // col: weight + pane ids, top to bottom + panes: []Pane // slot array; id = index + active: Pane.Id + drag: Drag // none | select | move | border | scrollbar + theme: u2 // index into themes + colors_on: bool + surface: Surface // rebuilt by surface(), arena-backed + effects: Fifo(Effect) + +Pane + tag: TagLine // live prefix (mode, cwd/path) + editable tail + kind: union { term: Term, file: File, image: Image } + vweight: f32 + mode: enum { normal, insert, tty } // helix-modal; tty = raw to the pty + cursor: absolute body position // rides the scrollback, not the screen + sel: [3]Sel + line/char modal sels // per-button block sels, msel, vsel + edits: Splice // typed runs anchored to absolute rows + undo: edit snapshots // unified undo across term edits and file content + +Term = ghostty-vt Terminal + its stream (bytes in via Event.output) +File = path + bytes + line index + Syn (tree-sitter highlight bytes) +Image = decoded RGBA + petscii grid cache +``` + +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 is lines of code; the number +only goes down. As built, against the prototype's 12,072 lines of Zig: + +#table( + columns: (auto, auto, 1fr), + stroke: 0.4pt, + [*module*], [*lines*], [], + [core (pardes.zig + look.zig + syntax + image + dump)], [4,289], [all semantics, all platforms], + [modal.zig + petscii.zig (pure, unit-testable)], [1,410], [carried over, unchanged], + [shells (tty.zig; gui.zig = sdl native + web)], [2,768], [translate + rasterize only], + [main.zig + build.zig], [484], [three targets, codegen for queries], + [*application total*], [*7,626*], [vs 12,072 — three full backends instead of one and a half], + [snapshot harness (snapshot.zig + e2e_harness.zig)], [831], [the parity oracle, kept], +) + += 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 cover the +checklist in Appendix A; 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 (that SDL draws the grid it was given, that vaxis diffs +correctly) is out of snapshot scope and covered by each shell's single smoke +test. + += Build + +`zig build` produces three statically linked artifacts: `pardes` (vaxis), +`pardes-gui` (SDL3), `pardes.wasm` + shell page (emscripten). Same dependency +set as the prototype — ghostty, vaxis, uucode (shared config), zstbi, SDL +(castholm, lazy), zig-tree-sitter + grammars — all via `zig fetch`, wired in +`build.zig`. Codegen during build, consumed by comptime: tree-sitter +`highlights.scm` → options module; tutor text → embedded pane content. 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 +config files. 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 fourth shell +could serve `Surface` and `Event` over 9P without touching the core. + += Appendix A: feature parity checklist + +From the prototype survey; every line is covered by at least one of the +eighteen event scripts in `test/snapshots/` (boot, tty, edit, scroll, modal, +look-file, look-dir, exec, tag, theme, tutor, windowops, dump, load, ttyonly, +syntax, fileedit, images). + +*Layout.* Columns by weight (≤6), panes by vweight (≤16); 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; `--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 pane cwd +(`/proc/pid/cwd`) or file's dir; 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), +image ext → image pane. 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 $ ^ gg ge gh gl G`, +`Ctrl-d/u/f/b`, `zt zz zb`), insert entries (`i a I A o O`), `v`/`x` +selections, `d c y p u U`, Enter=look Tab=execute at cursor. 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). `p` pastes from system clipboard (OSC 52 request/reply); yank +mirrors out via OSC 52. + +*Tag.* Live prefix (mode indicator, cwd or path) + editable tail with the +full modal editor; defaults `Del` / `Save Del`; image tags are toggle words +(`Petscii C64|Term Ascii`). Topbar: `Kill Newcol Tutor Debug Colors NextColor +Dump` — execute-only (left click inert). + +*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; 25 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). Tutor: embedded text as file pane. + +*Chrome.* Themes dark + acme-light (terminal-native vs paletted); 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, pinch + touch (two-finger = middle), render +scale, link-look opens a new tab (new — the prototype has no web link +handling). SDL shells: stb_truetype atlas over the SDL GPU API (SPIR-V on +native, GLES3 on web), letterboxed web scaling, two-finger drag = wheel, tap += middle click, pinch, touch debug overlay, `PARDES_TEST` PPM capture and +`PARDES_TEST_GRID` text-grid capture. Joystick cursor for the steamdeck is +*new* scope — the prototype ships no gamepad input. -- cgit v1.3