diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /docs/design.typ | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-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.typ | 216 |
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 |
