diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/config.md | 132 | ||||
| -rw-r--r-- | docs/design.typ | 216 | ||||
| -rw-r--r-- | docs/helix-keys.md | 4 | ||||
| -rw-r--r-- | docs/macos.md | 176 | ||||
| -rw-r--r-- | docs/web.md | 24 |
5 files changed, 437 insertions, 115 deletions
diff --git a/docs/config.md b/docs/config.md index 30962ddb..7e27216b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -16,15 +16,39 @@ still resolves, because "nothing is there yet" is the answer `Config` exists to give. There is one case with no path at all: a native launch with no `HOME` set, which `Config` reports as such. -`Config` (`SPC f c`, or the word executed anywhere) prints the resolved path -into a `+Config` output pane, so the machine answers this rather than the list -above. The path is printed whether or not a file is there — that is the case -you ask in — and the row is ordinary text, so a right click on it opens the -file. +`Config` (`SPC f c`, or the word executed anywhere) opens one refreshable +`+Config` pane. It reports the startup path and every live config-like value: +theme, colors, wrapping, tag position, debug mode, the requested shell and the +executable actually resolved at the last spawn, requested/effective GUI font +and size, tagline scale, panel transition, +scene effects, hover delay, platform, native-image support, and (on SDL) whether +the executable uses live-built shaders or the paired prebuilt shader snapshot. +Platform-dependent rows say `unsupported` instead of looking like an off or +empty supported setting: TTY reports Font and scene shaders as unsupported; +web reports Font, panel transitions, and scene shaders as unsupported. Tagline +font size is its own row and capability — GUI font selection is native-only, +while the browser still applies the compiled tagline percentage to its DOM. +The startup path is printed whether or not a file exists — that is usually when +it is most useful — and is ordinary selectable text, so a right click on it +opens the file. Shell follows the same requested/effective/pending model as +Font. `Compiled default shell` is the command built into the binary; `Shell +effective (last spawn)` is the executable the native host really chose after +installation lookup and fallback. A changed request remains pending until a +terminal is spawned, because the core does not resolve native executables. + +The mutable global values live together in the plain `runtime_config.State` record. +One plain capability record gates the setting registry, leader table, +`EffectCode`, and report; the compile-time setting table generates both setter +builtins and their `Config` rows. Exhaustive checks require every table-backed +toggle, transition, and scene-effect switch to occur exactly once, so those +generated setting builtins cannot quietly lose their query row or leave a +renderer switch unnamed. Manual pane-local actions remain with their payload (for +example an image tag reports its renderer choices); they are not global +configuration. The browser build has no local user-config path and does not load this file. -(Nor does it have the `Font` builtin, or a language backend, or ptys of its -own — see `docs/web.md`.) +(Nor does it have `Font`, the effect builtins or `EffectCode`, a language +backend, or ptys of its own — see `docs/web.md`.) The format is one existing builtin command per line, using the same spelling and argument parsing as commands executed inside pardes: @@ -32,14 +56,16 @@ and argument parsing as commands executed inside pardes: ```text Theme acme Font DejaVuSansMono-Regular +TaglineSize 82 Shell zsh Wrap ``` A line matches a builtin whose name takes NO argument only as that whole word: `Kill` runs, `Kill something` does not. Builtins that take one (`Theme`, -`Font`, `Shell`, `Restore`, `Find`, `Grep`, `Rename`, `WsSymbols`, `Look`, -`Exec`) take everything after the name as the argument. +`Font`, `TaglineSize`, `Shell`, `Restore`, `Find`, `Grep`, `Rename`, +`WsSymbols`, `Look`, `Exec`, `EffectCode`) take everything after the name as +the argument. `Theme <name>` wants one of the 228 names in the ring. Do not derive the spelling — read it off `ThemeSel` (`SPC t t`), which lists every one as the @@ -60,6 +86,20 @@ terminal's font belongs to its emulator and a browser's to the page — so a resolve the name by walking the font directories on every lookup, so a face installed a moment ago is findable. +`Font` is asynchronous at the renderer boundary. `Config` therefore keeps +requested name/path, pending state, and the effective face/point-or-pixel size +as separate facts; a failed request never gets reported as the face on screen. +Taglines use a distinct face size in both native GUI renderers. Execute +`TaglineSize <percent>` to change it live, for example `TaglineSize 70`; the +accepted range is 1 through 100 and the default comes from +`gui_tagline_font_percent` in `src/config.zig` (82). The native renderer +remeasures both the glyph and its visible tag band while retaining the body's +cell grid. The 100% ceiling is deliberate: a tagline remains exactly one +logical grid row, so a larger face or band would overlap its pane body or a +neighbour instead of leaving the body grid stable. `Config` reports the active +percentage. The browser applies the same compiled percentage to its DOM glyphs +but has no runtime setter. + What happens to a codepoint the chosen face has no glyph for differs by shell. The SDL GUI falls back through a chain it builds itself: embedded Adwaita Mono, then installed `NotoSansMono-Regular`, `DejaVuSansMono`, @@ -76,3 +116,77 @@ bad line does not prevent later lines from running. Top-level text that is not a builtin is not sent to a shell. (`Exec ...` remains an ordinary builtin and therefore keeps its normal behavior.) Key bindings remain compile-time choices in `src/config.zig`; this startup file does not remap them. + +## Panel and scene effects + +Exactly one panel transition is selected at a time. Executing its builtin a +second time turns it off; selecting another replaces it: + +```text +PanelSlide +PanelZoom +PanelDissolve +PanelAscii +PanelVertical +``` + +All panel transitions start off. Slide uses cubic ease-out and zoom uses an +overshooting ease-out-back. Dissolve and ASCII compare the last successfully +presented grid with the new one: cells which did not change are immediately +canonical, while changed cells cross from old to new through smoothstep or +stable themed punctuation. Vertical is a pane-lifecycle effect: a newly added +pane rises from below inside its own fixed box, and a deleted pane's frozen +content rises out; surviving panes are never animated. The TTY implementation +performs those operations directly on a copy of the canonical cell grid. +The SDL and native macOS GUI implementations pass plain panel tracks to their +GPU shaders, including native image/PDF pixels; layout itself commits +immediately and remains the one authoritative geometry. DOM web intentionally +does not expose these builtins: its renderer is selectable HTML/CSS and has no +canvas or shader stage. + +The scene effects are independent switches and can be combined: + +```text +Crt +Ripple +Glitch +``` + +They share one full-scene shader pass in SDL and macOS. With all three off the +pass is bypassed. CRT works in linear light with restrained scanlines, mask, +bloom, curvature, and noise rather than remapping the theme to a strong fixed +palette; Ripple and Glitch primarily perturb sample coordinates. + +`EffectCode <effect-builtin>` opens the build-embedded effect math, host +paint/submission path, and backend shader/grid sources, for example +`EffectCode PanelAscii` or, in a GUI build, `EffectCode Crt`. TTY exposes it +for its grid transitions; native GUI builds +expose it for transitions and scene shaders. It is absent on web, where no +effect argument could succeed. Shared +passes are shown as shared source segments rather than manufactured per-effect +copies. The command works from an installed binary and does not need the source +checkout beside it. SDL output also labels its shader provenance. An ordinary +build prints the live GLSL that `glslc` compiled for that executable; +`-Dprebuilt-shaders` prints the tracked GLSL snapshot paired with the committed +SPIR-V instead and labels those segments with their `shaders/prebuilt/` paths. +`zig build shaders` refreshes both files of every pair together, +so editing live GLSL without that explicit refresh changes neither half of a +prebuilt executable. + +## Delayed Look preview + +The preview is enabled by default. Moving the pointer onto selectable text and +leaving it still for `look_preview_delay_frames` (2 animation ticks, roughly +33 ms at the 60 Hz animation cadence) paints a +subtle theme-derived preview of the exact operand a right-click Look would receive. +Repeated motion reports in the same semantic grid cell do not restart the +delay. The preview uses the same side-effect-free word/path expansion as Look; +it does not focus a pane, move a cursor, install a selection, activate a PDF +page, or execute anything. Motion to another operand, pointer leave, input, +pane teardown, and relevant content changes cancel it. + +Set this compile-time option in `src/config.zig` to disable the feature: + +```zig +pub const look_preview_delay_frames: ?u16 = null; +``` 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 diff --git a/docs/helix-keys.md b/docs/helix-keys.md index addc23da..2aff75cd 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -95,7 +95,7 @@ language-backend queries, and the shell pipe. | `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — normal/tty modes only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; kept alongside the leader because a pane in raw **tty** mode never sees `SPC` (the shell owns it), so this is the only keyboard way out of one. Insert mode owns `Ctrl-w` = delete-word-back, so a tag being TYPED into swallows it; from a tag in normal mode (`:`) it moves focus to the neighbour's BODY, while the bare letters `h/j/k/l` there move to its TAGLINE (next row) | pardes-specific | | `Alt-n` | new terminal below (any mode) | shadows helix `Alt-n` TS sibling-select — skipped anyway (tree-sitter) | pardes-specific | | `Alt-c` | move active terminal to a fresh column (any mode) | helix `Alt-c` is change-noyank; the pardes window op wins (do-not-touch contract). `Alt-d` + `i` covers the behavior | waived (`alt-c-window-op`) | -| `Space` (normal, body) | the pardes LEADER: a key path from here runs a BUILTIN with no arguments — the same builtins the topbar and the tags hold. `SPC k` Kill, `SPC d` Del, `SPC f s` Save, `SPC f n` New (an empty temporary file focused in the calling pane's column), `SPC f f` Find (fd over the pane's directory, results into `+Search`), `SPC f g` Grep (grep -R over the CONTENTS of every pane's directory, the ones another pane already covers dropped, rows relative to the asking pane's own directory, results into `+Search`; like Find it takes an ARGUMENT — `Grep foo` executed, or a selection chorded onto the word, searches that and skips the input), `SPC h t` Tutor, `SPC c n`/`SPC c d` Newcol/Delcol, `SPC y/Y/p/P/R` the five clipboard words (next row), `SPC t d/c/n/r` Debug/Colors/NextColor/Crt, `SPC t t` ThemeSel (the whole theme ring as `Theme <name>` rows in an output buffer — rows that are COMMANDS rather than places, so a theme is previewed by EXECUTING one, Tab or a middle click; `n`/`N` do not run them, they step past to the next look-able text on the ring), `SPC t p/l/a` Petscii/Palette/Ascii (an image pane's renderer toggles — the words its tag used to spell out, so the bar stays a bar), `SPC s d`/`SPC s r` Dump/Restore, `SPC w h/j/k/l` Left/Down/Up/Right (directional pane focus — the `Ctrl-w` prefix's four moves as builtins), `SPC j o`/`SPC j i`/`SPC j j`/`SPC j l` Back/Forward/Last/Jumplist (the jump stack: walk it back, walk it forward, hop to the pane before this one — what body-normal Esc runs — and read the stack as a clickable buffer). `?` at ANY depth opens the Help builtin listing what the prefix can still reach (`SPC ?` = all, `SPC h ?` = the docs group), into a `+Help` output buffer. The pending path shows at the right edge of the active pane's tag; Esc — or any unmapped key — abandons it | helix spends Space on pickers/LSP (section C); pardes has neither, and acme's builtins are what a leader is for. Enum-derived: the paths, the Help lines and the execute dispatch all fold out of one `Builtin` enum at comptime. Normal mode on a BODY only — tags are always insert, tty keys belong to the program | pardes-specific | +| `Space` (normal, body) | starts the pardes LEADER: a key path runs the same builtin words used by tags and the topbar. The main groups are `f` files, `h` docs, `c` columns, `t` toggles/effects, `a` panel animations, `s` session, `j` jumps, `l` language, and `w` directional focus; `SPC ?` lists every path and `<prefix> ?` lists one group in `+Help`. The pending path appears on the active pane's transient body/message row. Esc or an unmapped key abandons it. Paths, Help rows, and dispatch are all generated from the builtin registry at comptime; the leader applies only to a BODY in normal mode because tags and tty programs own their input | pardes-specific; helix spends Space on pickers/LSP (section C), while pardes uses acme-style executable words | | `SPC y` `SPC Y` `SPC p` `SPC P` `SPC R` | helix's clipboard menu on helix's own letters: yank the selection to the system clipboard (`ClipYank`) or the PRIMARY selection alone (`ClipYankMain`), paste the system clipboard after (`ClipPaste`) / before (`ClipPasteBefore`) the selection, replace the selection with it (`ClipReplace`) | the ONLY five words in pardes that touch the desktop's clipboard — `y`/`d`/`c`/`p`/`P`/`R` and the acme cut/paste chords are the internal register alone. Builtins rather than bare chords because a leader path names a builtin: they land in Help's index and are executable words like every other verb. One divergence: pardes keeps a single register VALUE where helix keeps one per range, so `SPC y` at N cursors joins them with newlines (`Pardes.setYank`, the divergence `msel-yank-paste` already waives). On a tty the write is OSC 52 out and the READ is OSC 52 back, which many terminals refuse or gate — so `SPC y` works there and `SPC p` can be a no-op | out of corpus | | a paste from the OUTER terminal | one `Event.paste`, spliced in at the cursor | the tty shell enables bracketed paste and coalesces `paste_start`..`paste_end` into a single event; before that the bytes arrived as individual key presses and normal mode RAN them, which is how a pasted `d` deleted a line. The bytes deliberately never enter the yank register — clipboard and default register are separate stores in both directions | pardes-specific | | `/` (any pane) | pardes' own plain-substring search into a `+Search` output buffer: the tag takes the pattern, Enter fills the buffer, and its rows are ordinary look targets. Enter also GOES to the first row — the buffer is focused and then the step `n` is and the look Enter is run in it (`Pardes.lookFirstHit`), so `/foo` lands on the first hit with the matched span selected. A pattern that matched nothing opens its empty buffer and moves nothing | KEEP, do not touch; not in the corpus (helix `/` is regex search). Find and Grep answer with OTHER files and deliberately do NOT jump. The stepping half is the next row | pardes-specific | @@ -348,7 +348,7 @@ Files (all in `test/hxcases/`): (one yank register, not one value per range), and `sel-regex-caret` / `sel-regex-dot-newline` (mvzr is not the Rust regex crate: no multi-line `^`/`$`, and `.` matches a newline). -- `test/hxdiff.zig` builds `pardes-hxdiff`, which drives the sans-IO core +- `test/hxdiff.zig` builds `pardes-hxdiff`, which drives the core headlessly at 80x24 (22 body rows, matching helix's 22 text rows). Run it: diff --git a/docs/macos.md b/docs/macos.md index 12e598b5..b800513f 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -44,6 +44,11 @@ the view and drives its own frame clock, and the ABI grows a `platform` pointer field carrying the `NSView*` — one field, because the rest of the seam does not change. Nothing here is designed to make that harder. +That does not rule out a *postprocess*. CoreText is still the renderer and the +cell ABI is unchanged, but shader effects draw that same frame into a retained +bitmap and feed it through Core Image kernels on one Metal context. +There is no second glyph atlas, pane renderer, or view pointer in the ABI. + ## Why the ABI mirrors src/web.zig The browser and a Cocoa app are the same host, and the browser proved the shape @@ -78,6 +83,11 @@ asking for a thin insert caret instead of a block. `Surface.images` crosses separately as `pardes_frame_images` / `pardes_frame_image_list` — see "Pixel attachments" below. +`pardes_scene` is the matching full-window snapshot: a bit each for CRT, +ripple, and glitch, plus the 60 Hz time/frame that animates them. It is returned +by value, so the renderer never holds a pointer into live core state. A zero +flags word is also the fast-path contract: draw the CoreText frame directly. + **Input.** `pardes_key` takes a codepoint and the host's already-composed text, because composition is AppKit's job and the core only ever wants finished characters. Keys that carry no text are the four ASCII controls (enter, escape, @@ -123,15 +133,19 @@ because it owns the filesystem side too. **Lifecycle.** `pardes_init` returns 0 or an opaque nonzero code, `pardes_deinit` tears down, `pardes_tick` drains pty output plus the effect queue and returns whether anything changed, `pardes_should_quit` reports the -Exit builtin or the last pane closing, and `pardes_animating` says a theme -transition wants ~60 Hz ticks until it settles — the one thing in an otherwise -event-driven frontend that redraws on a clock, handled in `tty.zig` by sleeping -the loop and forcing a tick. +Exit builtin or the last pane closing, and `pardes_animating` says +`pardes_animation_tick` wants a ~60 Hz call until it settles — the one thing in +an otherwise event-driven frontend that redraws on a clock. A persistent scene +effect deliberately keeps that clock armed until its builtin turns it off. +Ordinary input and pty pumps never count as elapsed animation frames. The ordering contract is the part a header cannot enforce. **Init with the real -grid size.** The core defers each shell's greeting until it has seen a resize: -`sync()` in `src/pardes.zig` only emits the opening `ls` once `resize_count > 0` -and the pty has produced its first prompt. Setting `Options.cols`/`rows` alone +grid size.** The core defers each integrated shell's greeting until it has seen +a resize: `sync()` in `src/pardes.zig` only emits the opening `ls` once +`resize_count > 0` and parsed OSC 133 B says the prompt has handed the cursor +to input. An unintegrated shell has no reliable readiness signal, so it skips +the cosmetic greeting; an explicit command still releases after its successful +fork. Setting `Options.cols`/`rows` alone never bumps that counter, so init must turn its arguments into an actual resize event the way `src/web.zig` does immediately after construction — and the size must be true, because the first `forkpty` takes its winsize from the core's @@ -252,12 +266,12 @@ most deliberate twist there is and the one a stale velocity sample would fling hardest. And a finger back down (`pardes_rotate(0)`) catches a coast in progress, the way a hand catches a dial. -The coast itself is spent by `pardes_tick`, one fixed 1/60 step per tick with a -0.94 decay, and it makes `pardes_animating` true for as long as it lasts — so -it rides the same 16 ms re-pump a theme transition does and needs no clock of -its own. Fixed rather than measured on purpose: one fling then spends the same -travel every time, which is what lets `rotate.snap` assert it instead of -asserting the machine's timer jitter. +The coast itself is spent by `pardes_animation_tick`, one fixed 1/60 step per +scheduled frame with a 0.94 decay, and it makes `pardes_animating` true for as +long as it lasts — so it rides the same 16 ms re-pump a theme transition does +and needs no clock of its own. Fixed rather than measured on purpose: one fling +then spends the same travel every time, which is what lets `rotate.snap` assert +it instead of asserting the machine's timer jitter. Both halves are goldens. `rotate.snap` turns the dial at 4° per 100 ms (40°/s, under the floor) and asserts the screen is byte-identical across the release, @@ -471,14 +485,18 @@ can disagree. `pardes_font_take` hands it over once, the same take-and-clear shape as the haptic, and the view loads it with `CTFontManagerCreateFontDescriptorsFromURL`, picks the untraited cut out of a collection, derives bold and italic from it, and re-measures. A file it cannot -wear leaves the screen exactly as it was — a terminal that cannot draw has no -way back out of itself. +wear crosses back through `pardes_font_reject` and leaves the old metrics +exactly as they were. Success crosses through `pardes_font_ack` with the +effective PostScript name and point size. Requested and effective values are +therefore distinct, queryable facts rather than a request being mistaken for +what is on screen. -Zoom does not touch the core at all. Cmd+=, Cmd- and Cmd+0 change the point size, -`Metrics` is rebuilt, and the new cell is reported through the same resize path -a window drag uses; the core reflows to a different number of columns and knows -nothing about points. Cmd+= rather than Cmd++ because AppKit matches the -character and `=` is what is under the finger. +Cmd+=, Cmd- and Cmd+0 change the point size, rebuild `Metrics`, and report the +new cell through the same resize path a window drag uses. They also observe the +effective face/point tuple through `pardes_font_observe`, so the single `Config` +report follows a host-owned zoom without resolving a pending face request. The +initial system face is observed the same way. Cmd+= rather than Cmd++ because +AppKit matches the character and `=` is what is under the finger. Both are machine-checked in `test/macos-snapshots/font.snap`, which needs two different kinds of assertion because a snapshot is the core's cell buffer and @@ -551,18 +569,40 @@ Turning this on also changes what an IMAGE pane is here: it was the PETSCII glyph-art fallback, the same one a terminal without kitty graphics gets, and it is now the real pixels. +## Live file reload + +`FileWatcher.swift` gives each watched pane a vnode source on the file and one +on its parent directory. The file catches in-place writes; the parent is +essential because editors commonly save by renaming a fresh inode over the +path. After the 45 ms debounce the file source is rearmed on the current inode, +then the event returns to Zig with the pane's watch generation. Replacing or +closing a pane advances that generation, so a callback already queued for the +old occupant cannot reload the new one. + +The callback only wakes the ordinary main-thread pump. `pardes_tick` checks the +exact owned path, filtering unrelated changes in the same directory, duplicate +vnode events, and Pardes's own saves. Text panes compare and adopt a bounded +byte snapshot by content hash. PDFs can be much larger than that bound, so they +compare inode/size/time metadata and let MuPDF reopen the pathname directly. +That identity is committed only when equal stats bracket a successful +transactional reopen; a mismatch receives one self-scheduled retry, so it does +not depend on a second vnode edge and cannot spin forever on a malformed stable +file. Reading settings survive and derived page data is regenerated. A +malformed PDF therefore leaves the last good document usable and remains +retryable after the next real write. + ## Themes, live Two bugs lived here, and they were the same bug. `ChromeTheme` fades between themes over ten 16 ms steps, advanced by a `.tick` -event. The tty and SDL loops call `core.update(.tick)` on their own clocks; -this host has no loop of its own, so nothing advanced it — `pardes_tick` -drained ptys and reported `themeAnimationActive()` back without ever stepping -the transition. The fade therefore never moved and never ended: every tagline -kept the *previous* theme's colours until the next launch, and the 16 ms -re-pump in `AppDelegate.pump` spun at 60 Hz for the rest of the session. The -pump is the clock, so `pardes_tick` steps it. +event. The tty and SDL loops call `core.update(.tick)` on their own clocks; the +native host schedules the same clock explicitly through +`pardes_animation_tick`. `pardes_tick` only drains work: if every input pump +also advanced the transition, a burst of key or pty events could collapse a +ten-frame fade into one display frame. The scheduled callback advances once, +then pumps effects and redraws, and re-arms itself only while +`pardes_animating` remains true. `pardes_theme_bg` is the other half. The window background behind the titlebar and behind a live resize was a hand-agreed `#121212` in two files; it is now @@ -571,6 +611,65 @@ chrome's, because document backgrounds switch the instant the theme does while chrome fades. `window.appearance` follows its luminance, so wearing `acme` no longer leaves a dark titlebar over a cream grid. +## Scene effects + +`Crt`, `Ripple`, and `Glitch` are three switches over one postprocess, not three +stacked filters. Their canonical macOS source is `shaders/crt.ci.metal`, which +the build installs as `pardes.app/Contents/Resources/crt.ci.metal`. The Swift +shell loads that exact asset with `CIKernel.kernels(withMetalString:)` and runs +it through a `CIContext` created from the system Metal device. The source is +also a Zig build import, so `EffectCode` can print what the app executes when +the source checkout is absent. + +The ordinary CoreText/attachment/cursor pass is one function. With any scene +bit or panel track it targets a retained, backing-scale bitmap; kernels then +sample the complete frame into the flipped view. PDFs, image panes, taglines, +rules, and the caret therefore receive the same effect. With every bit off and +no panel track the bitmap and Core Image context are bypassed entirely. Ripple +and glitch alter sample coordinates only, preserving theme colors; CRT works +in linear light with restrained bloom, scan/mask, vignette, hum, and noise +instead of applying a broad color remap. + +The scene kernel alone receives a `clampedToExtent` image and the final output +is cropped back to the original finite extent. Coordinate tears and chroma +samples therefore clamp to the edge exactly like SDL's scene sampler instead +of acquiring transparent-black seams from Core Image's finite source image. + +The Zig side owns the clock. `pardes_animation_tick` increments its wrapped +60 Hz frame only while a scene bit is active, and `pardes_scene` derives seconds +from that integer. Input bursts cannot accelerate the shader. + +Pointer input follows the same destination-to-source transform as the last +presented scene frame before it is divided by the cell metrics. The view keeps +that exact `pardes_scene_s` snapshot and mirrors the Metal barrel, ripple and +glitch sampling arithmetic in `ScenePostprocessor.sourcePoint`; pixels outside +the CRT tube have no cell. The resulting displayed-grid cell then reaches the +core, whose panel-track mapping resolves it to canonical pane content. + +## Panel transitions + +`pardes_frame_panel_track_list` publishes the core's plain `Track` records +without a host-side animation model: pane serial/id, phase, effect, frame, and +logical-cell `from`/`to` boxes. The C layout and every field offset are asserted +against the Zig extern struct on Linux. The exported list is already stable +paint order—moving panes by slot, then opening panes by slot—so Swift only +consumes it. + +CoreText still renders one canonical complete frame. When tracks exist, the +same retained bitmap used by scene effects is fed through two kernels in +`shaders/crt.ci.metal`: `pardesPanelClear` removes every final target first, +then `pardesPanel` samples each target into its eased presented box. Slide uses +ease-out cubic, zoom uses ease-out-back, dissolve uses stable pane/cell noise, +and ASCII materialization replaces not-yet-revealed cells with procedural +punctuation. Because the input is the finished bitmap rather than a glyph-only +batch, backgrounds, glyphs, taglines, rules, the caret, PDF pages, and image +panes move and dissolve together. Scene CRT/ripple/glitch runs once after the +panel composition. + +With no scene bit and no panel track the retained bitmap, Core Image context, +and Metal passes are bypassed. `EffectCode Panel*` embeds this actual Metal +file plus its direct Swift owner, the same files the app executes. + ### Transparent themes A theme with `bg = null` — the curated `dark`, and every vendored @@ -794,7 +893,12 @@ vocabulary (`start`, `wait`, `stable`, `text`, `key`, `snap`, `command`, exists here: `fingers <n>`, `force`, `rotate <degrees> [gap_ms]`, `rotate_end`, `drop <path> <col> <row>`, `scroll <rows> <col> <row>`, `haptic <none|exec|look>`, `nsclick` (the AppKit-event path, as opposed to -`click`'s direct entry-point call), `font <name>`, `zoom` and `clipboard`. +`click`'s direct entry-point call), `font <name>`, `font-size <points>`, `zoom`, +`tracks <phase:effect>...`, `draw-effect`, and `clipboard`. The boot script uses +`tracks` followed by `draw-effect` to assert the moving/opening ABI order and +that the runtime Metal owner actually accepted the panel frame (a raw fallback +fails); the font script checks every initial/adopted/zoomed effective point +size without changing its grid goldens. The dial's two extras are what make momentum testable at all: the optional gap is a real sleep before the event, so a script can say how FAST the dial is being turned, and `rotate_end` @@ -815,6 +919,11 @@ every pixel comes out identical, which is what keeps `draw(_:)` honest — `snap reads the core's cell buffer and would be perfectly happy with a `draw` that returned on its first line. +The Linux loop proves the new C layout, flag encoding, clock wrap, embedded +kernel source, header syntax, and static library. Compiling Swift, runtime Metal +kernel compilation, and comparing processed pixels remain `macos-e2e` work on +a Darwin host; Linux has neither AppKit nor Apple's Metal runtime. + Goldens are hermetic: a fake `$HOME` with a pinned `PS1`, `Shell bash` in the config (fish's prompt carries a hostname), `LC_ALL=C`, `PARDES_NOTIME=1`, and `TMPDIR` inside the per-script world so that `New`'s document has a reproducible @@ -851,6 +960,11 @@ this matter, are both in `pardes_frame` rather than in the view — it re-render every cell of the grid on every frame, and `draw(_:)` ignores its `dirtyRect` for exactly that reason. +Those figures are the direct path with all scene bits off. An enabled scene +adds an offscreen CoreGraphics frame, one Core Image/Metal kernel, and +presentation of its result. That opt-in cost has not been measured on the M2 +used for the table and is not folded into the direct-path claim. + ## Not implemented - **The `lsp` effect.** Needs a worker plus a snapshot of the pane's path and @@ -862,12 +976,6 @@ for exactly that reason. - **The `pipe` effect.** Selection filters need `pipeRequest(id)` copied into a job, a worker to run the command, and a `pipe_resp` event back. Same shape as `lsp`, one more response type. -- **The `watch` effect.** `watchPane` is inotify and returns silently off Linux - (`ponytail:` at `src/tty/tty.zig:1183`). macOS wants the FSEvents half of - `std.Build.Watch`, which the standard library already has as - `Build/Watch/FsEvents.zig`. Without it the core simply never receives - `file_changed`, a state it tolerates because the browser has no filesystem - either. - **IME and marked text.** Only finished characters reach `pardes_key`, so a dead key composes nothing and Option is Alt rather than a compose modifier. Real composition means implementing `NSTextInputClient` *and* giving the core diff --git a/docs/web.md b/docs/web.md index ddc09d35..a6e2e923 100644 --- a/docs/web.md +++ b/docs/web.md @@ -18,6 +18,22 @@ 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. +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 +tagline glyph using the build-time `gui_tagline_font_percent`; the percentage is +exported by the module rather than duplicated in JavaScript. Animation time is +also host-independent: `requestAnimationFrame` time is accumulated into 60 Hz +core ticks, so 120/144 Hz displays do not accelerate frame-count transitions +and a returning background tab has bounded catch-up work. + +The `web` platform is deliberately not one of the shader-capable native GUI +shells. It exposes no `PanelSlide`/`PanelZoom`/`PanelDissolve`/`PanelAscii`/ +`PanelVertical` or `Crt`/`Ripple`/`Glitch` builtins: applying those faithfully +would require a second canvas renderer and give up the DOM renderer's +selectable/accessibility contract. Theme fades and the delayed, +side-effect-free Look hover remain grid animations and continue to use the +fixed 60 Hz ticks above. + Build a replay from a dump: ```sh @@ -37,12 +53,20 @@ file-or-directory — is native-only, because the wasm module roots at `src/web.zig` and never links `main.zig`. State comes from the embedded dump instead. +Web LOOK's source archive follows Git's working-tree view: tracked `.zig` files +plus new, nonignored ones, sorted by path. The checked-in browser replay is a +real native TTY dump of that same selection; its top and bottom launcher +snapshots jointly cover the complete list before opening `build.zig`. + What is genuinely absent is narrower than "no IO". The module has no threads and no host filesystem, so there is no startup config file, 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). +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, |
