summaryrefslogtreecommitdiff
path: root/docs/design.typ
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-16 15:49:12 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:44:42 -0300
commit1551e409c31992437cb2fa864f576d45c8433801 (patch)
treee2fae8451f87b735a1360c7c2e383fdc40165789 /docs/design.typ
parentbe2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff)
downloadpardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz
pardes-1551e409c31992437cb2fa864f576d45c8433801.zip
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'docs/design.typ')
-rw-r--r--docs/design.typ216
1 files changed, 146 insertions, 70 deletions
diff --git a/docs/design.typ b/docs/design.typ
index 7ee9f1ac..8dd14cdf 100644
--- a/docs/design.typ
+++ b/docs/design.typ
@@ -43,21 +43,35 @@ its own renderer; the core owns everything the user would recognize as pardes.
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
+ gamepad files, a C font loader and eight 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).],
)
+Native shells share `shell_bin`'s OSC 133 startup snippets, but not their
+files. Each host owns a private `mkstemp` pair for its lifetime, writes and
+closes both before the first fork, passes those unpredictable paths directly
+in child argv, and unlinks them at teardown. Concurrent tty, SDL and macOS
+launches therefore cannot truncate, source, or replace one another's startup
+files.
+
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.
+platform reconstructs the panes—terminals by replaying their raw VT streams
+into fresh emulators, files, images and PDFs from their bytes. Editable tag
+tails are stored separately from their dynamic live prefixes, and image records
+retain PETSCII, palette, and ASCII renderer choices. 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. The reader validates weights, scroll
+ranges, pane references, and bounded tag tails before constructing anything;
+invalid base64 fails instead of silently becoming empty content. A PDF record
+whose path cannot be opened—or a build without MuPDF—falls back to an ordinary
+file pane with those exact embedded bytes and its original editable tail. Same
+core, no viewer fork.
The boundary is two data types, both plain values:
@@ -76,7 +90,8 @@ 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
+cursor, pixel attachments, and a bounded list of plain panel-transition tracks.
+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
@@ -84,8 +99,9 @@ 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.)
+graphics when available. Transition tracks identify their pane by slot and
+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}`,
@@ -95,9 +111,18 @@ The core never performs IO for any of these; it asks — and four of the asks ha
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
+`watch` returns `file_changed` whenever the shell notices a text file or PDF
+moved under it. The tty and SDL hosts currently implement that path with Linux
+inotify; the native macOS host watches both each file and its parent directory
+with debounced DispatchSources, then restats the exact path under a pane-generation
+guard. The file catches in-place writes while the parent follows rename-over
+saves. Text snapshots are filtered by their content hash; PDFs use bounded
+inode/size/time identity and commit it only when equal stats bracket a
+successful transactional MuPDF reopen. A mismatched transaction gets one
+bounded self-retry. This catches rename-over saves without reading a large PDF
+merely to notice it changed or spinning on a malformed one. Web has no
+filesystem watcher. A PDF response retains its
+reading position and pane settings. 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.
@@ -111,10 +136,20 @@ 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.
+There is one such file by design: `look.zig` holds path/`:line` resolution,
+URL detection, and the per-platform outcomes. The core's one `pointerOperand`
+primitive owns click-word expansion and is shared verbatim by right-click and
+the delayed hover preview; that policy stays beside input because it also
+observes live pane selections and wrapped grid coordinates.
+
+Pane implementations are similarly flat and direct: `file_pane.zig`,
+`term_pane.zig`, `image_pane.zig`, `output_pane.zig`, and `pdf_pane.zig` own
+their kind-specific storage and operations. `pardes.zig` keeps the layout,
+input dispatch, cross-pane invariants, and the small calls joining those
+modules. There is no pane vtable or callback layer; the kind is already plain
+data, so a direct switch/call is the shortest boundary.
+The large end-to-end PDF cases live in `pdf_pane_integration_test.zig`, keeping
+pane-specific fixtures and raster assertions out of that core file as well.
= Data structures
@@ -123,24 +158,76 @@ 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.
+User-settable runtime choices are one plain `runtime_config.State`: booleans,
+theme index, owned bounded shell/font strings, requested/effective font facts,
+one panel-transition enum, and scene-effect booleans. A compile-time `settings`
+array generates each setting builtin and the rows of the single `Config`
+query. It has no callbacks and no parallel query registry to drift from it.
+
+Horizontal layout weights are fixed-point integers and geometry rounds
+cumulative boundaries. Splitting a column replaces only its weight `W` by
+`A+B=W` at the same position. Therefore every boundary outside the source
+column is bit-identical before and after the split, including at awkward
+non-dyadic screen widths; only the source and new column can receive movement
+tracks. Vertical splits apply the corresponding rule to the source pane's
+weight.
+
+= Presentation animation
+
+`panel_animation.zig` is backend-neutral data and math: five transitions,
+their easing, exact endpoint progress, stable per-cell noise, and a POD track.
+The core detects opening/moving rectangles when it commits layout and publishes
+only active tracks. It retains the last successfully presented canonical grid
+and a semantic old/new cell mask; unused grapheme bytes do not manufacture a
+change. A separate dense closing-track list is presentation-only state for a
+pane whose functional lifetime has already ended. Pointer input inverts the
+presented slide/zoom/vertical rectangle back to the canonical grid, lets
+unchanged dissolve/ASCII cells through immediately, and rejects closing
+pixels, so pixels and gestures cannot disagree during a transition.
+
+TTY copies the canonical `Surface` grid into a compositor scratch grid, clears
+slide/zoom destinations, then paints moving, opening, and closing panels in
+order.
+Cleared geometry uses the theme page color when it is explicit and the host
+terminal default only for transparent themes, so a light theme cannot flash a
+dark gap. Slide/zoom change the copied rectangle. Dissolve changes only diff
+cells from their old value to their new value; ASCII takes the same cells
+through punctuation; vertical raises only an opening or frozen closing pane
+inside its own clip. SDL supplies old/new glyph data, diff flags,
+final/presented boxes, and effect parameters to the glyph and native-image
+shaders. macOS passes the same records across its plain C ABI and composites
+old/new panel images in Metal/Core Image. Scene `Crt`, `Ripple`, and
+`Glitch` bits share one full-window pass in each native GUI. DOM web is a
+separate platform, not a shader GUI: retaining selectable HTML/CSS is more
+important than duplicating the renderer in canvas, so it exposes neither
+effect family.
+
+`EffectCode <effect>` writes the actual backend math, host submission, and
+shader/grid source segments embedded by the build into an ordinary output
+pane. This makes the implementation inspectable
+after installation and makes sharing explicit: several builtins can quite
+honestly print the same shader with different uniform bits.
+
```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
+ settings: runtime_config.State // theme, shell/font, display/effect choices
rects: [16]Rect // where each pane landed, this frame
+ panel_tracks: [16]?Track // live panes, serial-guarded
+ closing_panel_tracks: [16]Track // dense visual tombstones, no pane owner
+ presented_cells + changed_cells // acknowledged baseline + semantic diff
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
+ file: ?file_pane.State / image: ?image_pane.State / pdf: ?pdf_pane.State
+ // payload presence is the kind; none = terminal
vweight: f32
mode: enum { normal, insert, tty } // helix-modal; tty = raw to the pty
// `v` adds a fourth thing to DISPLAY, "select",
@@ -148,13 +235,14 @@ Pane
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
+ ovl: ?term_pane.EditBuffer // ONE typed run, anchored to an absolute row
+ undo: two stacks, not one — term_pane.Snapshot history for an edit buffer,
+ // and file_pane.State history for file content
-File = path + bytes + line index + Syn (tree-sitter highlight bytes)
-Image = decoded RGBA + petscii grid cache
-PdfView = MuPDF document + per-page rasters + cached outline
+file_pane.State = path + bytes + line index + Syn (tree-sitter highlight bytes)
+image_pane.State = decoded RGBA + petscii grid cache
+pdf_pane.State = MuPDF document + continuous layout + search/selection/outline
+ + bounded per-page raster relay + frame placement decisions
```
Layout is arithmetic, not objects: columns are weights over the width, panes are
@@ -170,35 +258,11 @@ 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.
+The rewrite used source size as a pressure toward direct code, and that remains
+useful when a refactor deletes duplicate policy or state. A checked-in line-count
+inventory does not: it goes stale whenever a pane kind, backend, or generated
+asset moves. Measure the current tree when making that comparison; keep this
+document about ownership and invariants that should survive the next edit.
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
@@ -214,14 +278,18 @@ 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
+checklist in Appendix A at the rewrite; ninety 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
+greeting `ls` waits for the exact OSC 133 B input mark after the real resize
+(the prototype raced bash's startup and won only by allocator luck); shells
+without prompt integration omit that cosmetic greeting rather than guessing.
+Commands which create a fresh shell are owned by its terminal pane until the
+host reports the actual prompt capability, then wait for the same mark when it
+exists. 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
@@ -252,12 +320,14 @@ 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 →
+sources → the generated half of the theme ring; working-tree `.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
+also the only one whose output is committed: `zig build shaders` refreshes each
+paired `shaders/prebuilt/*.spv` binary and `.glsl` source snapshot together.
+`-Dprebuilt-shaders` embeds that exact pair rather than shelling out, so
+`EffectCode` cannot describe different shader text from the binary on screen;
+this is also 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.
@@ -292,7 +362,8 @@ 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
+the source (cursor row kept visible); `Newcol` takes width only from the source
+column and cannot resize any unrelated column; 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
@@ -321,8 +392,10 @@ 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.
+left selection appended as trailing CLI argument. A stationary pointer gets a
+delayed, theme-derived highlight of the exact side-effect-free selection that
+Look would expand; it neither focuses nor installs that selection, and pointer
+leave/input/content invalidation cancels it. 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`),
@@ -349,9 +422,10 @@ 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:
+full modal editor; defaults `New Del` / `Save New Del`; an image tag reports
+`img petscii:<on|off> palette:<commodore|terminal> ascii:<on|off> <path>`
+before the ordinary tail (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.
@@ -408,10 +482,12 @@ 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
+open the current contents of tracked or new/nonignored 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
+a running Pardes TTY after
+`git ls-files --cached --others --exclude-standard -- '*.zig' | sort`, so its
+complete terminal listing is the same tracked-plus-new/nonignored 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