summaryrefslogtreecommitdiff
path: root/docs/design.typ
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-05 20:21:50 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitde4def3548a6729b0dfd2120495a61beef8c8c2c (patch)
tree12144f0f64dc96bb4741459c1f9db57f44349330 /docs/design.typ
parent7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff)
downloadpardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz
pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip
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).
Diffstat (limited to 'docs/design.typ')
-rw-r--r--docs/design.typ246
1 files changed, 246 insertions, 0 deletions
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.