# Startup configuration Native pardes builds use a per-user `pardes` configuration directory. Its main command file is named `init`: - Unix: `$XDG_CONFIG_HOME/pardes/init`, falling back to `~/.config/pardes/init`. - macOS: `$XDG_CONFIG_HOME/pardes/init` when that variable is set, otherwise `~/Library/Application Support/pardes/init`. - Windows: `%LOCALAPPDATA%\pardes\init`, with `%USERPROFILE%\AppData\Local\pardes\init` as the fallback. On the two unixes `XDG_CONFIG_HOME` counts only when it is ABSOLUTE, as the XDG base-directory specification requires; an empty or relative value falls back to the home-directory form (`config.User.path`, and the test beside it). Windows never consults it. An `init` that does not fit the `max_bytes` read limit — 1 MiB, `config.User` in `src/config.zig` — or that cannot be read at all is treated as no file: `load` takes the `readFileAlloc` error and keeps going. The path 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 (or, on Windows, neither `%LOCALAPPDATA%` nor `%USERPROFILE%`), which `Config` reports as `no per-user config path`. `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, focus tint, syntax weight, 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, window opacity, ligatures (SDL GUI only), 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. The fields of `config.Runtime.Capabilities` gate them and are stated once as plain data in `builtins.capabilities`: `font_picker` is the SDL GUI and native macOS only, `scene_shaders` the same two, `panel_transitions` every hosted shell, `window_opacity` the SDL GUI and macOS, `window_blur` macOS only, `ligatures` the SDL GUI only, and `tagline_font_size` everything but the TTY and the board. `ligatures` is the exception to `unsupported`: where it is off, `Ligatures` is not a builtin at all and `Config` has no row for it. So the TTY reports Font, TaglineSize and the scene shaders as unsupported; the browser reports Font, panel transitions and the scene shaders as unsupported, and its TaglineSize row reads `82 (build-time only)` — tagline font size is its own capability precisely because GUI font SELECTION is native-only while the browser still applies the compiled percentage to its DOM glyphs. 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. `Default shell` is the one used while no `Shell` is set: `$SHELL`, the user's login shell, else `/bin/sh` (also when `$SHELL` names nothing executable); an explicit `Shell` overrides it. `Shell ` is refused unless it names an executable file (`Shell: shell "x" not found (...)`, or `Shell: not a shell: /etc is a directory`; a bare name is looked for in the usual bin directories), as `Tty ` is, and a bare `Shell` goes back to the default. The root ctl reads `Shell `. `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 `config.Runtime` 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 `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: ```text Theme orchard 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. A builtin that takes one (`takes_arg` in `src/builtins.zig`, or a `settings` row whose action is `shell`, `theme`, `font`, `tagline_size` or `window_opacity` in `config.Runtime.settings`) takes everything after the name as the argument. On a native build that is `Theme`, `ThemeFile`, `Font`, `TaglineSize`, `Shell`, `Save`, `Restore`, `Attach`, `Mount`, `Unmount`, `Find`, `Grep`, `Rename`, `WsSymbols`, `Look`, `Exec`, `Msg` and `EffectCode`. The SDL GUI also has `WindowOpacity`, which takes one argument. (`Peek`, `Poke`, `Hexdump` and `Gpio` take one too, but they exist only where `builtins.Board.enabled` holds, and that build has no config file.) `Theme ` wants one of the names in the compiled ring. Do not derive the spelling — read it off `Themes` (`SPC t t`), which lists every one as the exact `Theme ` line that selects it. `slug` in `tools/gen_themes.zig` lowercases, folds punctuation runs to a single `_` and then TRIMS leading and trailing ones (`penumbra+.toml` is `penumbra`, not `penumbra_`), and every variant read out of a zed `.json` gets `_zed` on the end so it cannot collide with a helix theme of the same name — zed's "Ayu Mirage" is `ayu_mirage_zed` and `ayu_mirage` is helix's `ayu_mirage.toml`. The suffix goes on all of them rather than only the eight that clash today, so a name cannot move when either project gains or loses a file. A name that is not in the ring is ignored. The fifteen [native Pardes themes](themes.md) lead the ring: `orchard` (the default), `dusk`, `ink`, `paper`, `daybreak`, `atelier`, `forge`, `lagoon`, `solarium`, `spectrum`, `harvest`, `clay`, `forge_black`, `forge_soft`, and `orchard_black`, then `acme` and `lapis`. `Themes` lists them first under Pardes themes. They add coordinated focus, search, diagnostic and terminal colors; `ink` and `daybreak` are high contrast dark and light options. After them come the [faithful ports](themes.md#faithful-ports) of well-known themes, a section per family, then the legacy `helix` and `dark`, then everything imported. Where a port takes an imported theme's name (`dracula`), the imported one gains `_helix` (`dracula_helix`), as zed's carry `_zed`. `NextColor` walks the same ring in the same order. `FocusTint` toggles the focused pane and column tag tints; it is on by default. Workspace, column and pane command text can be edited directly; see [editable tags](tags.md) for naming and saved-workspace behavior. The column command row is always shown; an old `ColumnTags` init line is ignored with a message. `Placement acme` (the default) puts new panes where acme would, in the active column; `Placement pardes` brings back pardes's own rules, which open a first document in a column of its own. See [where new panes go](tags.md#where-new-panes-go). `BootShell keep` (the default) leaves a shell alone when a document is dragged into the left column beside it, whatever it holds; `BootShell replace` closes such a shell there when it is the column's only one and nobody has typed into it (no scrollback, cursor still on the first prompt line), the boot's placeholder giving its rows to the document. Bare `BootShell` flips it, as bare `Placement` does: a setting that chooses among words (`on`/`off` switches, `Placement`, `BootShell`, `Crt`, `Bloom`, `Vignette`, `Grain`, `Lift`, `Motion`, `ShaderAnimation`) steps to its next value when written bare, as its word clicked in a tag does, and a value it does not take is refused with the values it takes, which `/commands` also lists. `Verbose` toggles the message-row announcement every builtin makes of its own name before it runs; it is on by default, and the builtins that own the message row themselves (`Msg`) never announce. Turning it off leaves the row to the messages a builtin chooses to write. `MessageAnimation` toggles how a message comes and goes; it is on by default. A message eases down into its row, fast at first and settling at the end, its colour whole by half way (graphical frontends slide it out from under the tagline as it fades up; a terminal only fades it in), stays until the next key or click as it always has, then lingers for `MessageLinger` milliseconds (default 800) before it dissolves into the page. `MessageFall` (default 180) and `MessageDissolve` (default 150: leaving is quicker than arriving) set how long the fall and the dissolve take, also in milliseconds; each timing is at most 60000, and `Config` reports all three. `MessageLinger 0` with `MessageAnimation off` restores the old behaviour, a message cleared by the very input that follows it. An updated line on a row already showing one swaps its text in place rather than falling again. `SyntaxBold` toggles bold syntax keywords; it is off by default. These settings are shared by GUI and TTY, and `Config` reports their current states. Like `Colors` and `Wrap`, these commands take no argument and invert the current value. A `FocusTint` line in a fresh startup configuration disables the tint; a `SyntaxBold` line enables the stronger keyword weight. Those two appearance controls leave focus, selections and editing behavior unchanged. `Ligatures` (SDL GUI only) toggles a font's programming ligatures, such as Maple Mono's `->` and `!=` drawn across their cells; it is on by default. Off, every cell draws its own glyph, exactly as a font without ligatures does. `Ligatures on` and `Ligatures off` set it explicitly. The native macOS shell draws CoreText ligatures of its own, which this setting does not reach. `TreeContext` toggles sticky declaration headers for the current source pane. It is off by default and appears in the default pane tag when a tree-sitter grammar supports that file. `TreeContext on` and `TreeContext off` set it explicitly. Scrolling inside a function, type or module keeps its enclosing declarations above the body, with source line numbers, syntax colors and a subtle background tint. Clicking a header moves the cursor there without scrolling; scrolling upward reveals that source line as the headers recede. `Dump` and `Restore` preserve the setting per pane. Customized tags retain their text; the command can still be executed from any source pane. `TreeContextTagStyle` toggles the experimental tagline treatment for those headers and is on by default. In graphical frontends it uses the tagline font, line height and thin border. A matching separator marks gaps between the headers' source lines. Turning it off restores body-sized context rows. `TreeContext` itself still defaults off; this appearance option does not enable it. `Config` reports the appearance option, and dumps preserve it. Search, Grep and LSP location-result panes include `LocationsConfig` in their default tags. Custom tags keep their edits. Location highlighting is enabled by the command that produces a location list. Plain reports, including `LocationsConfig`, keep ordinary text colors even when their text resembles a location. `Mini` keeps its own syntax colors. LSP references and goto results highlight the exact symbol span supplied by the server, using the search-result colors. Source indentation is retained so those byte ranges stay aligned; context and descriptive labels remain unmarked. `LocationsConfig` prints the current settings for Search, Grep and LSP location results in an output pane. Execute the printed line to apply it again, or supply just the fields to change: ```text LocationsConfig context:5 tscontext:on tslocations:off layout:stacked ``` - `context` is the number of source lines above and below each match (default `0`). Overlapping context is shown once. These neighboring lines omit their locations and keep their code aligned with the matching result. - `tscontext` includes enclosing tree-sitter declaration headers in source order (default `off`). Preceding neighboring context stops at the outermost enclosing declaration, keeping unrelated lines above it out of the result. - `tslocations` shows a location on the first line of each declaration header (default `on`); continuation lines keep the same alignment without repeating the location. Declaration headers use the same muted color as locations, with or without their locations visible. - `layout:stacked` (default) puts each location on its own line, followed by its source preview. Hidden context locations do not add an empty address line. Use `layout:inline` to put locations beside the source instead. In inline layout, result locations share padding in groups of eight matches, so a long path only widens its own group. Context does not count toward the eight; at group boundaries, it aligns with the nearer match (ties stay with the preceding group). Source indentation is preserved. All visible locations start flush left; `n` and `N` still stop on matches. With `tscontext` enabled, asterisks after a match location show its declaration depth (for example, `main.zig:42 ***`). Ordinary neighboring source lines retain syntax colors; declaration headers do not. A declaration context line ends with ` ...` when source lines are omitted before the next displayed row from that file. Source analysis is reused across result queries while the source bytes stay the same. The cache retains at most 64 files and 64 MiB; it checks current buffer or filesystem contents on each refresh. Open file buffers supply context from their current edits. Missing files still leave the original result available. These settings apply to subsequent result generation and survive `Dump`/`Restore`. Invalid fields reject the entire update; if a field appears twice, its last value wins. `WindowOpacity ` controls the opacity of everything in the SDL window except text and the cursor, which stay fully opaque. Use `WindowOpacity 85` to see the desktop through the editor, or `WindowOpacity 100` to restore full opacity (the default). The argument must be a whole number from 0 through 100; missing or invalid values leave the setting unchanged. At zero only text and the cursor remain visible. Add the command to `init` to persist it. The same opacity applies to editor and embedded terminal backgrounds, UI chrome, borders, scrollbars, gutters, and images. Overlapping non-text drawing does not make those areas more opaque. Regular text, syntax colors, tagline text, terminal glyphs, and the cursor keep their normal opacity. This does not blend foreground colors into their cell backgrounds. The TTY and other non-SDL hosts do not emulate this effect; their `Config` report says `WindowOpacity unsupported`. On native Wayland, Pardes uses an alpha-capable transparent surface. It does not use whole-window opacity protocols such as `wp_alpha_modifier_v1`, because those would also fade the text. Presentation uses the existing GPU offscreen renderer followed by a readback and SDL renderer upload per presented frame, which adds rendering cost. Other SDL drivers can report background transparency as unsupported unless their rendering configuration is alpha-capable. If the rendering backend cannot apply the request, Pardes reports the error and keeps the last successfully applied opacity. `Config` reports the percentage and marks a request as pending until the SDL host handles it. ## Crash records A panic appends to `crashes` in that same directory, beside `init`, and only then prints to stderr (`src/crash.zig`, wired into the panic handlers in `main.zig` and — because the macOS build roots there — `macos.zig`). stderr is the one place this program cannot keep a trace: in the TTY shell stderr IS the screen, so the trace lands on a grid the terminal is being reset out of; the SDL and AppKit shells have no terminal at all; and a `--detach` session's stderr goes wherever its launcher left it. The file is appended, never rewritten, and each record is two lines — build metadata, then the panic message: ```text pardes 0.0.2 (a1b2c3d) 2026-09-03T11:20:44Z linux-x86_64 pid 48812 panic: index out of bounds: index 4, len 4 ``` NO STACK TRACE, and that is a measured decision rather than an omission. The frames stay on stderr, where `std.debug.defaultPanic` prints them. Collecting them here instead HANGS the process: `writeCurrentStackTrace` called from a panic handler before `defaultPanic` has run wedges at 0% CPU, and `captureCurrentStackTrace` — which looks like the safe half — takes `SelfInfo`'s rwlock exclusively on its first call, so a panic inside the walk leaves that lock held and `defaultPanic` then waits on it forever. What makes `defaultPanic` survive the same hazard is its own private `panic_stage`, which nothing outside `std.debug` can reach. A crash that becomes a hang is worse than the crash, so this file keeps only what it can gather without asking the process any questions: which build, when, where, and what it said. Everything about it is best effort and silent: no config directory (a launch with no `HOME`) means no file, and a directory that cannot be created or opened leaves the panic exactly as it was before — stderr alone. The directory itself is created if it does not exist, because the user who never wrote an `init` is as likely as any other to hit a bug. One record at a time: two threads panicking at once would otherwise interleave into one buffer, so the second falls straight through to stderr. Only panics come here; a SIGSEGV is caught one level lower (`main.zig`'s `debug.handleSegfault`) and unwinding one needs the signal's saved CPU context. ## Runtime theme files `ThemeFile ` loads one complete theme from a `.zon` file. An absolute path is used as written; a relative path is resolved from the `pardes` configuration directory, not from the process working directory. A typical layout is: ```text ~/.config/pardes/ ├── init └── themes/ └── mine.zon ``` and the corresponding init line is: ```text ThemeFile themes/mine.zon ``` After a successful load, hosts with document live reload watch the path with the same parent-directory mechanism, so in-place writes and editor-style rename-over saves reload the theme live. A malformed or incomplete save does not replace the last valid theme; fixing and saving the file applies the next valid snapshot. Selecting a compiled theme with `Theme ` or `NextColor` stops the custom-file watch. Execute `DumpThemes` to write every theme compiled into the executable to: ```text /themes/builtin/.zon ``` The command replaces those generated reference files but leaves unrelated files alone. Copy one into `themes/`, rename it, change its `.name`, and use it as the starting point for a custom theme. The dumped file is also the complete format, and it is `pardes.Theme` serialised by `std.zon.stringify`: the theme name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`, `box`, `box_dim`, `kw`, `str`, `num`, `comment`, `lineno`, `scroll_track`, `scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or `null`. Optional nullable RGB roles extend this format: `tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `empty_col`, `lineno_active`, `search_bg`, `search_fg`, `diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, `diagnostic_hint`, `tag_sel_bg`, `tag_rule`, `box_border` and `box_dirty`; `sweep_bg` and `sweep_fg` take three triples, and `rule_px` and `rail_px` a pixel count. Missing new roles use backward-compatible defaults, so previously exported files remain valid. [Theme customization](themes.md) describes each role and its fallback. RGB values are three-byte arrays, and hex literals are accepted. The original fields remain required; there is no inheritance or partial override layer. `tag_name_fg` gives the filename at the end of a pane's path a separate foreground. `tag_active_name_fg` can adjust that tint for active tags; it falls back to `tag_name_fg`. When both are omitted or `null`, the filename uses the corresponding tag foreground. All canonical themes use a different hue at similar perceived brightness to the surrounding text in both states. Directory text and tag commands retain their regular colors; selecting filename text uses the selection colors. Terminal tags use this color for `Tty`, which comes immediately after the path. `Filter` in a terminal's tag projects that pane's ANSI colors through the active theme, and does it in two stages. The default foreground and background roles are mapped FIRST, because ghostty-vt generates the whole 256-color projection from that pair; every other color follows, by reducing it to its nearest canonical xterm key and reading the key back out of the projection. That reduction compares RGB triples, so it knows about hue and nothing about the page — and the projection's cube corners ARE the two anchors, which is how a foreground used to end up painted the exact color of the paper behind it (`\x1b[38;2;255;255;255m` on acme's `#ffffea`, and the ANSI black a shell writes with `\x1b[30m` on either dark theme). So a foreground additionally has to keep `tty_filter_min_contrast` — a WCAG ratio, `1.5` by default — against the mapped background. One that cannot is not mapped: it takes whichever of the theme's own two anchors is still visible on that background. Backgrounds are exempt, since a background is the page the floor is measured against. Set the constant to `1.0` to accept every projected color, collapses included. `Shell ` sets the binary that the NEXT terminal pane execs; panes already open keep the shell they are running. A bare name is resolved against the handful of directories a shell actually lives in, not `$PATH`. On Linux, `Tty9p` (`SPC n 9`) starts that shell with a private kernel 9P mount, asking sudo inside the new terminal. `$PARDES_MOUNT` names the mountpoint. The installed `pardes-v9fs` helper lives beside the editor; development builds can set `PARDES_V9FS_HELPER` to its absolute path. See [v9fs.md](v9fs.md). Ctrl-B switches between raw TTY and editor mode. Plain Escape at a detected shell prompt hops back to the previous pane. Other keys, including Ctrl-O, Ctrl-W and modified Escape, belong to the child. Use `Mode` in the pane tag to return to editor mode in place. Desktop paste events still feed the terminal. The paste chords are the exception the window keeps. Ctrl-V types the yank register at the program, and Ctrl-Shift-V asks the desktop for its clipboard and types that; both go through the program's bracketed paste when it has asked for one. Neither reaches the child as a keystroke, so an application that would otherwise answer Ctrl-V by reading the system clipboard itself never gets the chance to read the wrong thing. `Font` and `Fonts` exist ONLY in the SDL GUI and native macOS builds — a terminal's font belongs to its emulator and a browser's to the page — so a `Font` line is one of the silently-ignored ones everywhere else. Both builds resolve the name by walking the font directories on every lookup, so a face installed a moment ago is findable. Use `Font :` to change face and size together, for example `Font MartianMono-NrRg:18` in the startup file or an editable tag. Fractional sizes such as `:18.5` are supported; the accepted range is 8–72 (pixels in SDL, points on macOS). `Font ` without a suffix preserves the current size. Invalid sizes or unknown faces leave the current font unchanged. The SDL GUI also has a tiny optional workspace-tag companion: `Pet cat`, `Pet frog`, or `Pet off` (the default). Its original pixel sprite walks and idles in the trailing blank area of the global tag only. It hides while that tag is being edited, when the tag is full, or when the font leaves too little room; it never replaces text, receives clicks, or appears in pane/column tags. Animation runs at ten steps per second, uses theme ink, and requires no image assets or shaders. Put `Pet cat` in the startup configuration to keep it; `Pet off` disables the companion and its animation. Other hosts ignore the startup command. `Config` reports the current choice in SDL. `Font` is asynchronous at the renderer boundary. `Config` therefore keeps requested name/path/size, 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 ` 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 body-grid pane geometry. Graphical tag text uses the smaller face's measured monospace advance and fills the available pixel width, including workspace and column tags. Text selection and scrolling use that same capacity; drag grips retain their physical body-grid width and position. 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. Both native GUIs separate the reduced-height global and pane tagline bands with a `gui_topbar_pane_border_px` physical-pixel rule. Set it to zero to leave it out. `gui_topbar_pane_border_rgb` can pin an RGB color; its default `null` follows the theme's `border` role in SDL (falling back to `scroll_track` for older themes), and `scroll_track` in the macOS shell. Every band is centred in its row, so the workspace, column and pane tag text share one baseline offset and the anchors get the same margin above, below and to the left. With `Tagbottom` enabled, a tagline on the final grid row is bottom-aligned so the unused half-band does not show beneath it. The rule lives in the core (`pardes.taglineBandOffset`), and the macOS shell reaches it over the C ABI rather than keeping its own copy. When the window is not a whole number of cells, bands and rules at the right and bottom edges run on through the leftover pixels. 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`, `SymbolsNerdFont-Regular`, `NotoSansSymbols2-Regular`, `NotoSansSymbols-Regular`, and `DejaVuSans`, in that order, missing entries skipped; the rasterized glyphs are retained in its GPU atlas. The macOS shell has none of that and needs none. It embeds no font. Its default face is `NSFont.monospacedSystemFont`, resolved through the descriptor rather than by name, and `Menlo` only if that face turns out not to be fixed-pitch — a proportional face in a fixed grid is a broken screen, not a cosmetic problem (`PardesView.defaultFace`). A `Font ` arrives as a PATH the core already resolved by walking the font directories, and a file CoreText cannot measure leaves the face already on screen rather than substituting one, because the alternative is a terminal with no way back out (`PardesView.fromFile`). Per-codepoint fallback is CoreText's own cascade at draw time. What the shell caches is `CGGlyph` ids, not pixels. Blank, unknown, malformed, or unsuccessful lines are ignored silently, and a 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 PanelEdges PanelFall PanelWave PanelCurtain PanelScramble PanelType ``` All panel transitions start off. To disable one, execute its builtin again: `PanelDissolve` turns off an active dissolve, and `PanelAscii` turns off an active ASCII transition. `Config` shows the active command under `Panel transition`. Remove that command from your startup configuration to keep it off after restarting. Executing a different transition enables that one instead; these commands are toggles, not an unconditional animation-off command. 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. Dissolve switches visually changed cells at stable noise thresholds. ASCII walks every changed single-byte printable glyph from its old `u8` value to its new one, spending that byte distance as the frames of the walk. The walk is eased in and out: a glyph creeps at both ends and crosses the middle of its distance in a few large skips, inside the same number of frames a constant one-value-per-frame walk would have taken. Glyph-stable style changes and non-ASCII graphemes become canonical immediately. A walk is capped at twelve movement frames, and each pane lasts only as long as its longest walk. The core computes and composes that semantic diff once for every backend; pixel attachments, which have no character value, pass through unchanged. Six further transitions are character *motion* over the same frozen/new grid pair, and are composed in the core the same way: - `PanelEdges` — whole rows slide in from alternating screen edges. - `PanelFall` — columns rain down into place, each with its own head start. - `PanelWave` — a vertical ripple travels across the pane and decays. - `PanelCurtain` — a wipe, left to right, behind a soft edge two cells wide. - `PanelScramble` — every cell churns through printable ASCII and locks onto its final glyph at its own stable noise threshold. - `PanelType` — reading-order reveal with a caret sitting on the write head. Unlike dissolve and ASCII these carry *every* glyph in the pane, changed or not: text flying in from a screen edge has to bring its unchanged glyphs with it. A cell whose glyph has not arrived shows the frozen old cell rather than a blank or a blend, so every intermediate frame is made of real characters. No cell is a valid input target until its own glyph has settled. 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 drops back down; surviving panes are never animated. The TTY implementation performs its remaining geometry/dissolve operations directly on a copy of the core-composed presentation 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 effect is `Crt`, at a level from 0 (off) to 3; `on` is 2: ```text Crt Crt 3 ``` The SDL GUI runs it as the bundled pass of its post chain, which also takes Shadertoy files written for ghostty (`Shader ~/crt.glsl`, `Shader off`; a file compiles again when it is saved, and a save that fails to compile keeps the last good one and says why), and `ShaderAnimation off|on|always` says when the chain animates on its own. A GUI attached to a detached session runs the session's chain the same way. With the chain empty the pass is bypassed. CRT works in linear light with restrained scanlines, mask, bloom and vignette, and no curvature, so clicks land where they are drawn. Its slow hum and dither move on their own, so with Crt on the GUI keeps drawing while idle (under `ShaderAnimation on`, while focused); the other bundled passes are still and let it rest. Three more bundled passes take the same levels, each off by default and each costing nothing while off: ```text Bloom 2 the brightest ink glows a little (only what is brighter than the page; a dual Kawase blur at half size and down) Vignette 2 the window's corners fall a little into shade Grain 2 the page's own ground takes a fine, still grain, like paper ``` None of them touches a tag, a grip, a notice or the cursor, and each is capped per theme so text and the selection keep their own contrast or 4.5 (docs/effects.md). The focused pane can stand off the page (SDL GUI, off by default): ```text Lift shadow a soft drop shadow, on the other panes' bodies only Lift rim a hairline just above the focused tag (light, or shade on a light page) Lift auto a shadow on a light page; on a dark one the others recede Lift off InactiveDim 30 the unfocused panes' text fades 30% toward its ground Motion smooth off, crisp, smooth (the default), bouncy or playful SelectionGlow on a soft halo of the selection's colour round it in the body, fading in over 100 ms, never over a tag, a grip or the cursor HoverGlow on a soft glow under the word a look-hover would open, fading in over 80 ms Occlusion on pane bodies darken faintly toward their edges (2%), never over the cursor Parallax on a theme's page pattern (lapis's dots) moves with the text at a quarter of its speed JumpTrail on in a terminal, a jump of the cursor of three cells or more leaves a trail of a few cells that fades in 120 ms (truecolor terminals; a pixel shell glides instead) ChipShadow on in a terminal, a notice chip casts a shadow a cell right and down: half blocks on blank cells, a darker ground on text ThumbFlash on in a terminal, a pane's scroll thumb brightens as it scrolls and fades back over 250 ms CursorBlink on the cursor blinks, solid while typing and half a second after, eased at each edge, solid after 10 idle seconds GripWidth 150 the grip's button and the scrollbar under it, percent of the theme's rail_px (12px at a 17px tagline; 50 to 300) ``` `InactiveDim` works everywhere (a grid shows it at once) and never takes a pair below its own contrast or 4.5. Under `Lift auto` on a dark page it is 30 while unset. `Motion` sets how every such effect moves: see docs/effects.md. `EffectCode PanelAscii` or `EffectCode Crt` lists the current backend's build-embedded source paths under `/virtual`. Look opens each full file; no checkout is needed, but the build must carry them (`-Dembed-sources=true`, off by default); otherwise the command reports them unavailable. TTY exposes grid transitions, native GUI builds also expose scene shaders, and web has neither. Shared implementations share paths. SDL reports whether GLSL was compiled during this build or came from the `-Dprebuilt-shaders` snapshot paired with the committed SPIR-V. `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, and `animation.frame_ms` is 16, so about 32 ms — 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; ``` ## Build-time configuration Runtime settings live in `src/config.zig`. Build options are listed below; `zig build --help` lists the options available for the selected platform, including the standard `-Dtarget` and `-Doptimize` options. | option | values | default | |---|---|---| | `-Dplatform` | `tty`, `gui`, `web`, `macos`, `esp32p4` | absent builds the tty cli and the SDL gui together | | `-Dstatic` | bool | `false` | | `-Dquic` | bool; 9P over QUIC using system OpenSSL 3.6+ | `false` | | `-Dmupdf` | bool | on for a native target, off for web and esp32p4 | | `-Djpx` | bool | `true` — JPEG 2000, and with it scanned PDFs | | `-Dtree-sitter` | `disabled`, `zig`, `minimal`, `full` | `full` natively, `zig` for web, `disabled` for esp32p4 | | `-Dtheme-animation` | bool | on everywhere except `-Dplatform=esp32p4` | | `-Dworkspace-tag` | bool; draw the workspace tag row — the macOS shell hands its commands to the native menu bar instead | on except on `-Dplatform=macos` | | `-Dprebuilt-shaders` | bool | on for a bare `zig build`, off when `-Dplatform` names a shell | | `-Dtracy` | path to a Tracy source checkout | off | | `-Dmacos-identity` | codesigning identity for `pardes.app` | `-` (ad-hoc) | | `-Ddump` | a `dump.zon` to embed in the web shell | none | | `-Dtest-filter` | substring; run only tests whose name contains it | none | | `-Dtest-rebuild` | bool; force fresh Zig test compilation, retaining cached C dependencies | `false` | | `-Dhelix-harness` | native reference executable for live differential tests | `HX_HARNESS`, otherwise `hx-harness` on PATH | | `-Desp32p4-cols` | u16, the board's grid width in cells | `56` | | `-Desp32p4-rows` | u16, the board's grid height in cells | `14` | The local board build emits an object. Firmware clock, serial port and profiling options belong to the sibling `05-zig-p4` toolchain's build. Two build inputs reach the running binary as ordinary values rather than as behaviour. `build.zig` reads `.version` from `build.zig.zon` through an untyped `@import("build.zig.zon")` — one place to bump — and `gitCommit(b)` reads the revision at configure time. Both land in `pardes_config` and are re-exported as `pardes.version` and `pardes.commit`, and `pardes --version` prints `pardes ()`, or `pardes ` alone when there is no commit: a tarball, a container with no `git`, or a checkout outside version control all yield null, and the flag has to work anyway. Neither is a question asked at runtime — a binary that shelled out to `git` would describe whatever tree it was standing in rather than the one it came from. `Restore a.dump` first looks for the relative path in the dump directory: `DumpDir ` when set (a leading `~/` is your home; bare `DumpDir` returns to the default), else `$XDG_DATA_HOME/pardes` or `~/.local/share/pardes`. `Config` reports the directory in effect as `DumpDir `, so the line can be fed back as configuration. If it is absent, Restore uses the argument as a path as before. Absolute paths and argument-free Restore retain their existing behavior. `Dump` still honors `$PARDES_DUMP` and otherwise writes a timestamped file in the default directory. A dump keeps each text pane's dot (its selection, or the caret), as acme's keeps a window's q0 and q1, and a Restore puts it back where the kept view shows it. It keeps the settings that differ from a fresh session's (`Placement pardes`, `Verbose off`, a shader), as the root ctl reads them, and a Restore sets them again; the theme is kept with the layout, and the font stays the frontend's. REPL bindings are not kept. A restored terminal comes back live: its recorded output (the last MiB of it) is replayed as history, a dim `── restored history ──` line marks where it ends, and a new shell starts below it in the directory the old one was in, the same shell it ran (a `Tty bash` pane comes back running bash). A view left scrolled back stays where it was. Only the shell is new; nothing the old one was running is restarted. A command pane comes back finished, with what it printed and its `exit N`, and is not run again (a command still running when dumped comes back as `exit ?`). The web shell, which has no ptys, still shows the history alone.