# Changelog ## 0.24 - Edit runs all of acme's commands: `< | >` through your Shell (one undo step, a failure changes nothing, its stderr in +Errors), `X` and `Y` over the open panes, `b B D e r w f` and the `"re"` file address. A pane a running filter changes refuses Del, Undo, Get and Zerox (busy), and a flushed or dropped Edit write kills its commands. - Edit's `w` to a file changed on disk is refused once, as Save is (`w again to overwrite`). - A look at a directory opens a pane listing it, as acme's directory window does: its entries in columns, a directory's marked `/`. A look at an entry opens it from there, and `Get` or a look at it again reads it again. `DirLook terminal` types `ls` into a terminal there, as before. - One Look path: Enter, Tab and a bare Look take what a click at the cursor takes, a diff line's address included, and an event reader hears them as clicks. - Get reads a pane's file again, asking once over unsaved edits; `Get file` loads another, and Undo puts the old name and text back. Load is Restore. - Putall saves every pane with unsaved edits. Zerox opens a second pane on the same buffer, kept across Dump and Restore. - `Tab N` sets the tab width, 1 to 16. `Incl dir` adds directories a look tries for a name found nowhere else, `` too. - Enter, `o` and `O` keep the line's indent as it is, tabs included, as acme's autoindent does. - Back and Forward (Ctrl-o, Ctrl-i, SPC j o, SPC j i, the mouse's side buttons) keep to the focused file first; `JumpScope all` walks every place in order. `n` in a command pane walks its own errors first. - Alt/Option-click is B2 and Super/Cmd-click is B3, sweeps included, for one-button mice and trackpads; a terminal passes only Alt. - Shift-Esc always backs out to the previous pane, in every mode; Ctrl-b alone toggles raw. SPC q quits, as Exit. - A +Pager keeps its program's colours; `PagerColor off` pages it plain. - Every pane gets `$PARDES_MOUNT`, the session's directory under `9ns --mntgen`. - Editing a terminal's text in normal mode says it edits a copy, which Tab or B2 runs and the program's next drawing replaces. - A path typed into a tag survives Esc: Enter renames, a second Esc drops it. - The init file reads past a BOM, says each failing line with its number, skips settings only the other frontend has, and applies when saved in a pane. - `pardes --wait` with no FILE or a gone session, and `--detach` over a name in use, say so in words. - File reads say `is a directory`, `permission denied` or `not a regular file`; a device is never read to the stream limit. - A dump no longer embeds a picture or PDF that is on disk; Restore reads it from there. - Fixed: a mouse report past the window's top or left edge crashed the terminal frontend, losing unsaved text. - Fixed: an unsaved pane restored from an older dump could Save over a newer file without asking. - Fixed: a PDF with a JPEG image crashed ReleaseSafe and Debug builds. - Fixed: the window drew every frame after focus moved to a PDF while the cursor glided. - cloud9's 9ns writes in parallel, so an Edit filter can write its own ctl through a mount without waiting, and drops a dead mount's entry off its serving thread. - The docs are a website, pardes.0x4200.cafe (`zig build site`): recordings, a builtins glossary, a theme gallery, and static builds with checksums. pardes is ISC-licensed. ## 0.23 - Restore reads a clean file pane from disk, so a file changed since the Dump comes back as it is now. An unsaved pane keeps its text, and if its file changed on disk its first Save asks before overwriting. A dump holds an unsaved pane's text once, and a clean pane's not at all. - A Rename the server answers in this file alone, while another open file of its language still says the old name, is previewed and not applied (zls renaming at a declaration does this). - Grep and Find say when a limit cut their results (hits, bytes read, entries walked, directories skipped), never claim not found when one hid a match, and a failed search leaves the +Search as it was. - Language servers go with their children when reaped and when pardes exits. - A PDF's fit and tint and a pane's collapse are read in its ctl and kept in the dump; a PDF hit that is not on the page is a miss. - `pardes --wait FILE` with only $PARDES_9P set waits in that session. - A `./x` look that finds nothing is a miss; a line over 1 MiB is refused once; DumpDir, Mount dial addresses and /pager given a file are refused in words. ## 0.22 - Grep and Find skip a directory they cannot open and keep every other hit; the list ends saying how many were skipped. - Rename, references, symbols and call hierarchies first tell the language server about every open file of its language, so a cross-file Rename previews every file you have open. The preview is the pane the Rename answers, and Lspinfo says `not started yet` before a server's first query. - acme's address and argument refusals reach a mount as EINVAL (cloud9 552584a), and unit tests now fail on any EINVAL or ENOENT refusal whose words a mount would read as something else. - /pager expands `~`, resolves the path, refuses a directory you may not write, and a refused write leaves no stale answer. - A terminal's Petscii survives Dump and Restore; Petscii, Ascii, Palette, PdfFit and PdfTint take the value to set. - `:N:H` works on a PDF pane, `@pN:` looks resolve, a line past the end reads the same everywhere, and word looks share their directory's +Search. - The reference carries the full errno table, and the scripting chapter an insert-after-a-match recipe. ## 0.21 - A command pane is leased to the exec open that started its command while that open stays open: another client's command in the same directory gets its own pane, so a script reads only its own output. - /index names a command's pane `cmd`, not `term`. - A terminal whose full-screen program was killed is idle again once the shell draws its prompt, and the next run reads its output whole; busy there answers `busy alternate screen`. - `--` ends the options. A name under a directory you may not search or write is refused, permission denied, and a Save there says so. - Restore and ThemeFile failures say why in words, a ZON one with its line; a failed ThemeFile changes nothing, on a Restore too. - /pager refuses a missing or relative directory; `Tty+bash` works on a pane ctl; msg records carry no clock; more refusals reach a mount as EINVAL. - The scripting recipes make their own pane and work through its look and exec, and the Traps say why the root ones are shared. ## 0.20 - `pardes -` asks the new root file `/pager` for its +Pager and reads the answer on the same open, so another client's command meanwhile can no longer take the paged text into its own pane. - `--wait` waits on the pane its own look answered, so a file whose name has a backslash is waited on instead of returning at once. - The scripting recipes read look and exec on the open they wrote; the reference documents `/pager`, that Grep is a literal search, and that Delcol opens no +Unsaved. ## 0.19 - `pardes -` writes its text through one open of the body: input past 8 MiB used to keep only its last part. Any size up to 256 MiB arrives whole, and past that it is cut with a note. - A ThemeFile that fails to load changes nothing, and Restore and `-l` load a dump's ThemeFile again. - A 9P look drops a trailing `:` as a click does, and a look that fails leaves open no file it opened. - pty/run after `clear` answers what was printed from the clear on. - A log follower keeps the records queued just before a Restore. - Too many open files is EMFILE through a mount (cloud9 12d78be4). - A detached session with no frontend sizes its ptys in pixels too, so terminal images work there. - addr, dot and limit on a pane with no text, and a bad `size`, are refused in words. The scripting chapter shows a bash event helper. ## 0.18 - A bare `:N` or `:N:M` look addresses the pane it is looked from, from a click, `Look`, 9P or an event write-back. - `pardes -` keeps one `+Pager` per directory and refills it; it no longer takes the keyboard, collapses `\r` progress lines, drops NUL and BEL, and shows a diff (`git diff`, `git show`, `git log -p`) as a diff. A text it cannot show goes to stderr with why. systemd tools page through it too. - A pane's `event` gives a record in pieces across short reads, so a shell `read` loop works. - A `sel` write leaves the written text selected. A PDF's body keeps its lines. `+PdfSections` rows are `file:PAGE:SECTION`. Event write-backs check their origin and numbers. A bad setting value is quoted. - Every recipe in the scripting and setup chapters runs as written. ## 0.17 - A terminal pane draws the images its program sends with the kitty graphics protocol: yazi's image and PDF previews show inside pardes. The GUI draws them; the tty passes them to an outer terminal that speaks kitty graphics, and draws them as PETSCII where it does not. - `Petscii` toggles a terminal pane's images to PETSCII and back, and `TermImages real|petscii` sets it for new terminals. - A terminal answers the pixel size queries (`CSI 14/16/18 t`) and its pty carries pixel sizes, so programs scale images to the cell. ## 0.16 - Releases are numbered 0.N from here on; this changelog's earlier 0.0.N headings read 0.N. - `pardes -` reads stdin into a `+Pager` pane, and a terminal's `PAGER` and `GIT_PAGER` are `pardes -` unless your environment sets them: `git log` and `man` open as panes, and a paging command through `pty/run` no longer waits. Command panes page through `cat`. `Pager off` keeps your own pager. - A command run from a pane gets `$winid`, that pane's serial, as acme's do. - A PDF page taller than a raster is drawn at the size it is shown, in bands around the screen, instead of being squeezed into 4096 rows and enlarged: tall pages are sharp, and memory stays bounded. - A diff line's look, whether clicked, written to `look` or written back through `event`, goes through one expansion; a blank context line works. - `log` stats 0 and 9ns opens files of length 0 as streams, so `cat` follows the log through a mount. - A second `--detach=NAME` while NAME runs says so. `/index` escapes control bytes. `file:0:0` is refused. An event reader of a closed pane reads EOF. `+Unsaved` lands in a directory that exists. `pardes NEW` refuses a directory it may not write. - Docs: a "Setting up your environment" chapter (9ns, the editor and pager, yazi, shells, language servers, fonts, plan9port) after the guide. ## 0.15 - The documentation is one Typst corpus in `docs/typ/`: a guide (a short walkthrough: modes, buttons, tags, the active column, looks, terminals, diff review), the one-page cheatsheet, scripting over 9P, a per-file reference, themes, and building for contributors. It builds an A4 cheatsheet, a book PDF and an HTML page; README.md, the served README and the 9P skill are short pointers into it. The old docs/*.md are folded in. - The tutor follows the guide's order, its key and tag errors fixed; a test fails when the tutor or any docs/typ file names a word, key, leader path or default tag that no longer exists. - The cheatsheet draws mouse buttons as a mouse with the pressed part filled, keys as one pill per press (`Ctrl+w`, `g›g`), and its pairs as tables. ## 0.14 - A pane's `tagexec` reaches an event reader holding the pane as an `x` record, as a click in its tag does. - A shell command from the workspace or a column tag runs as a command pane in the session's directory, never typed into the terminal with the keyboard. - A PDF's `:N` look turns to page N. `>` into an empty body is one undo step. A file never on disk comes back clean from Restore. - `pardes @p999` from a pane is refused when the session has no such pane; forwarding failures are sentences. - `name` takes any bytes but newline and NUL, so what it reads writes back. - A session removes its 9P socket and its prompt rc files when it exits or is killed, and sweeps those of dead sessions at start. - Generated files (`data`, `xdata`, `index`, `layout`, `recent`, `listeners`, `screen`) stat 0; `Edit w` fails in words; `/os` rename is permission denied; with several REPLs bound the pane always asks; a URL look in a detached session with nothing attached opens no browser. - Docs: how a relative look resolves (the pane's directory, then the jump list's), `event` takes one reader, and bash's `printf` writes a mounted ctl one line at a time. ## 0.13 - A one-page A4 cheatsheet, built from Typst sources in `docs/typ/` (`zig build cheatsheet`); a test fails when it names a word, key or tag that no longer exists. - A look on a diff line is the look of the `path:line` it names, exactly as if that text were selected by hand: the same resolution, placement and errors. A patch cached outside any checkout opens the file the way a hand-selected path would, and the diff's own path finding and its messages are gone. ## 0.12 - A selection deep in a very long line no longer slows every frame: a 5 MB line with the selection at its end redraws in milliseconds, not seconds. - `Joincol` is refused when the joined column cannot give each pane its tag and 2 rows. Save on a PDF or image pane is refused. A builtin made for one kind of pane (Petscii, PdfTint, Togglettymode, …) is refused on any other; `Mode` and `TreeContext` say why when they cannot act. - After `ThemeFile`, `/ctl`, DumpConfig and a Dump read `ThemeFile `. DumpConfig's unsupported settings are `# … unsupported` comments, so the whole report writes back to `/ctl`. Every bad setting value names what the setting takes. - A refused write to `dot` changes nothing; an edit that changes nothing makes no undo step; the jump list never holds one place twice in a row; `Jumplist` refills its one `+Jumps`; a look miss cuts a long path in its middle. - A look past a PDF's last page is a miss; PdfSections on a PDF with no outline says so; a Rename that finds nothing says so and fails; a theme file error names its path; Recent keeps PDFs and images, and an open file's row is where its dot is now. - Over a program that tracks the mouse, B2 and B3 are pardes's execute and look again, sweeps and chords included; Shift-B2 and Shift-B3 go to the program as its buttons 2 and 3. B1 and the wheel stay the program's, and Shift keeps them pardes's. ## 0.11 - A look on a diff names a file as the repository does: git's `a/`/`b/` (or mnemonic `c/`, `i/`, `w/`) is dropped only where both paths of the `diff --git` line carry one, a `--no-prefix` diff's real `a/` is kept, a plain `diff -u`'s names are as written, and a missing file is said by that name (`Look: src/x.zig: no such file here`). ## 0.10 - A 9P write never moves dot's end: scrolling written text into view moves only the view, and dot shifts as acme's textinsert shifts it, so a `sel` write after a `body` append replaces only what was selected. - A restored pane whose file is gone from disk comes back dirty. - A 9P look keeps its leading blanks, so a diff's context line opens its file. `diff -u` in a command pane has its timestamps cut too. A deleted file's lines all say "deleted by this diff". - Focus reports reach a terminal's program when focus moves over 9P. An empty-range event write-back expands as a click would. - Exactly 4096 bytes fit in a tag; an addr, dot or limit pair past the text or out of order is refused, not clamped; `name` refuses a directory; a rename through a mount is EPERM; a refused placement spends no serial. - A terminal's word looks share one `+Search` per directory. `Save path` logs its `save`. Restore logs the absolute path it read. ## 0.9 - A terminal's program that tracks the mouse gets its clicks, drags and motion as well as its wheel, in the format it asked for; hold Shift for pardes's own select, execute, look and scrollback. Mouse reports carry Shift, Alt and Ctrl. - A program that asked for focus reports hears when its pane or the window gains or loses focus. Every paste into a terminal is bracketed when the program asked, with control bytes turned to spaces. The GUI's F1-F24, Insert, Print Screen, Scroll Lock, Pause and Menu reach terminals. - A diff reads as the code it changes: a `.diff` or `.patch` file, and a command's output that is a diff (`git diff`), colour each hunk in its file's language, its old and new side each parsed whole, and tint added and removed rows to the pane's edge with their `+`/`-` in the tint's hue. Only the hunks in view are parsed, once each. - A right click on a diff's `+++`/`---`/`diff --git` line opens its file, on a `@@` line the new file at the hunk, and on a hunk line's `+`, `-` or space the new file at that line (a removed line: where it was). Paths are the repository's; a file not here says so and opens nothing. - A write through a mount that fills a writer's buffer (a multiple of 4096 bytes) keeps its cut last line for the next write, so `seq … > exec` runs every line once. ## 0.8 - `~` and `~/` mean the home directory wherever a path is taken: look (`~/x:12`, `~/x:/re/`), `name`, `Save`, `ThemeFile`, DumpDir, Restore. - `pardes --wait` on a new file waits on the pane it made, however fast that pane is saved and closed. A session's socket appears only once it is listening, so an early client is never refused. - A wheel over a terminal whose program asked for the mouse goes to the program, as a report in the format it chose, or as arrow keys on the alternate screen with alternate scroll: codex, htop, less and vim scroll. - `pardes FILE` in a pane whose session answers never starts a nested editor: a refusal is printed and it exits 1. A tty pardes killed by SIGTERM or SIGHUP puts its terminal back first. - A 9P connection holds up to 128 waiting reads, and refuses the next in words; 9ns (cloud9 705be665) no longer deadlocks a mount under many held reads. - A backward search along one long line takes time in its length, not its square. A detached session no client watches draws no frames: 50 MB of output through `pty/run` takes 11 s, not over 30. - Save makes its file's missing directories, for its own name and `Save path` alike. `pane/new` takes rows from another column before refusing. - With no pane at all, the root `look` opens its file and the root `exec` runs in the session's directory. A refused `Newcol` logs only its error. - `Lspwhy` explains the last query in its own file and place; every `+Lsp` answer shares one pane per directory. - DumpConfig's information lines start with `# `, and a line starting with `#` runs as nothing. `winsize` refuses fewer than 2 rows. `look file:3:0` is refused. `Kill X` with nothing running is a message, not a failure. An event write-back needs no trailing newline. ## 0.7 - `lapis` draws its cells unblended and each plaque pixel once: about a third less GPU time a frame at 4K, pixel for pixel the same. `lapis_plain` is lapis on flat grounds, keeping its gold frames, plaque shadows and file-name shadow. - Fixed a crash at startup when the 9P socket path is longer than the system allows, and a leak when a terminal pane closes. - Pane, column and workspace tags share one set of write checks: the 4096 byte limit, control characters, and a refused `>` write leaves the tag as it was. Column and workspace tags read without a trailing newline. - A control character in a write to any ctl file refuses the whole write. - Restore never reuses a column serial; the log announces a pane's `new` before anything else about it. - `DumpConfig` prints each setting as the ctl word that sets it, and `unsupported` for one the frontend lacks. - `New` and `Tty` run from the workspace and column tags over 9P, with no panes too; `Help` and `Find` there act in the session's directory. - `pardes NEWFILE` forwards into a directory that does not exist yet; Save makes it. The Tutor pane is `/+Tutor`. - Long-path errors keep their reason; `name` refuses a component over 255 bytes; regex refusals of an unanchored branch say why; acme's `Tab`, `Indent`, `Local`, `Incl` and `Abort` are refused, not run as commands. - `Lspwhy` explains the last language-server query, whatever its kind. - Bulk body writes over 9P are about 15x faster per 60 KB write. ## 0.6 - A middle or right sweep in a column tag or the workspace tag runs or looks up the swept text on release, highlighted while it is held, as in a pane's tag and as in acme; a press used to act on the word under it at once. - `Config` (`SPC f c`) opens the startup file, `init`, in a pane, or goes to its pane when it is open. With no file yet, it opens an empty pane of that name, and Save writes it, making the directory first. The settings report `Config` used to open is `DumpConfig`, in a `+DumpConfig` pane. - The workspace tag reads `… Dump Themes Config Debug Exit`: `Themes` in place of `NextColor`, which stays a builtin, and `Config` beside it. A workspace tag you edited, and one a dump kept, stays as it is. - F1-F25, Insert and the keypad reach the GUI's terminal panes too: one key map serves the tty, the GUI and the board. - A session restored from a dump is built as a fresh one is: its column and workspace tags hold several selections again, and 9P mtimes are real. - Zig files use `zls` from `PATH` as a language server like any other; the in-process ZLS backend is gone, and the binary is about 9 MB smaller. - Dumps and init files from before 0.0.4 no longer load their old tag formats: a restored pane's tag tail is read only from `tag_tail`, and an old terminal's `TTY` tag or raw output tail is not replayed. `ThemeSel`, `FontSel` and `ColumnTags` are no longer recognised in init files or dumps (`Themes` and `Fonts` are the words). - The GUI always embeds its shader sources; the prebuilt GLSL copies are gone. `lspprobe` is `zig build lspbench -- probe`. ## 0.5 - Over a slow link the terminal build keeps at most one frame in flight: it asks the terminal for a status report after each frame and draws the latest state when the answer comes back. A 150-step wheel flick over a 60 ms, 250 KB/s ssh link settles in under a second instead of nearly four. A terminal that never answers turns this off after one second. - A frame that changes nothing writes nothing, and a blank cell whose ink alone changed is not sent again. - Every theme gives a tag's file name, `+Errors` and a terminal's `Tty+` name an accent: a theme that sets none derives one from its own palette. acme's is its navy `#000099`. - `Kill` on a command stops the command's shell before its jobs, so the line's next command can no longer start in between. - `pardes NEWFILE` inside a pane opens a pane named NEWFILE, as acme's `B` does, and `--wait` waits on it. A launch with no terminal says so. - `Newcol` shares out a narrowed column's rows again, or is refused when its panes' tags would not fit. - 9P: msize up to 64 KiB; a Dump holds 16 columns; DumpDir makes every missing directory; an error over 128 bytes keeps its reason and shortens the path; a tag write over its limit is ENOSPC. - The docs and the 9P skill are shorter, with one home per fact. `zig build monkey-gui` runs the GUI monkey. ## 0.4 - Drive the editor over 9P the way acme is driven. Panes take sam addresses (`/re/`, `#n`, `$-1`, `0/re/`), `L:C` and inclusive `L:C-L2:C2` ranges on `addr`, with `data` and `xdata`; `Edit` runs the sam command language as one undo step. `exec` and `ctl` buffer lines per open, and a line without a newline runs with its write. One rule for failure: a builtin that fails fails the write and logs one `err`, no `msg`. - New 9P files: `/layout`, `/tag`, `/tagexec`, `col//{tag,ctl,exec}`, `pane//tagexec`, `/recent`, ask/answer records, and a `size` ctl for detached sessions (160x50 by default). Listings freeze at their open, so a read in several chunks never splices two moments. - acme's ctl words (`name`, `put`, `clean`, `dirty`, `del`, `delete`, `dot=addr`, `addr=dot`, `limit=addr`, `mark`, `nomark`, `show`, `cleartag`) are accepted again, done by the files that replace them; a pane's ctl reads acme's fields in acme's order. `Put` saves and `Delete` closes; acme builtins pardes lacks are refused, never run as shell commands. plan9port's `9p write` always truncates, so piping into `body` replaces it: append with `>>` through a mount. - Command panes and a REPL binding; `pty/run` runs one line and answers its exit status. Every child starts with default signal dispositions. - `Recent` lists recently opened files, closed ones included, and persists in `$XDG_STATE_HOME/pardes/recent`. A Jumplist row for a closed file says `(closed)`. `+Unsaved` lists dirty panes. `ThemeSel` and `FontSel` are now `Themes` and `Fonts`; `Exit`'s refusal opens in an output pane. - `pardes --wait FILE` (`-w`) run inside a pane opens FILE there and returns only when its pane is deleted, as plan9port's `E` does: set `EDITOR='pardes --wait'` for fish's Ctrl-O, git and the rest. Without it, `pardes FILE` still hands the file over and returns at once. - Only a write logs an `err`: a refused open, create or remove fails with Plan 9 words (`permission denied`, `file does not exist`) and logs nothing. - The tag is a full text buffer: multi-line, with undo. - Helix keys: the skipped non-conflicting keys, full registers and per-range anchors, checked against the latest helix. - Deleting the last column leaves the window empty but for its tag, as in acme. A buffer whose file was deleted on disk counts as dirty. - 62 themes in 13 families, each as close as possible to its original, grouped by family in `Themes`; an `acme` theme taken from plan9port's source, with acme's grips, 2 px rules and a 1 px tag rule. Sizes follow display density. - GUI motion: `Motion` flavours (crisp, smooth, bouncy, playful, off), `Lift`, a cursor that glides and moves with its pane, smooth scroll, and optional Bloom, Vignette, Grain, SelectionGlow, HoverGlow and Shadertoy post passes that recompile on save. Still passes let the GUI rest; Crt keeps drawing. - The terminal: replies to capability queries, malformed escape sequences and lone modifier keys never become typed text; function keys, Insert and the keypad reach terminal panes; a lone Escape is Escape at once; a focus change no longer resends every blank cell. - Speed: the theme's chrome is worked out once, not per grapheme; typing tells `g.` where its edit ended; large body and data writes are linear; a pattern that opens with a literal jumps to it. `zig build perf-gate` fails any gesture more than 3x its recorded baseline. - Robustness: over-long paths, non-UTF-8 bytes and odd 9P input are refused, never a panic; seeded GUI, core and 9P monkeys (`zig build monkey-9p`) replay and shrink what they find. - A column can be empty, as in acme. `Newcol` makes an empty column and gives its tag the keyboard; closing a column's last pane, or dragging it away, leaves the column where it was with nothing in it, drawn as its tag over blank space. The `+New` placeholder that used to stand in an emptied column, and the machinery that swapped it out when something else arrived, are gone. Closing the session's last pane now quits. - New panes go where acme's makenewwindow puts them: into the active column (the one last typed or clicked in), filling it when it is empty, else under the text of a pane with room to spare, else halving the biggest pane; never into a new column. `Placement pardes` brings back the old rules, which open a first document in a column of its own. - A document dragged into the left column no longer closes the untouched boot shell there; `BootShell replace` brings that back. - Column tags are always shown: the `ColumnTags` setting is gone, and an init file's `ColumnTags` line is ignored with a message. - Carrying a pane or a column by its grip shows acme's box cursor in the SDL shell and a move cursor on the web. - `Del` takes a side. `Del k` and `DelAbove` give the closed pane's rows to the expanded pane above it, `Del j` and `DelBelow` to the one below. A bare `Del` from the keyboard on a pane with panes both above and below asks on its notice band, and one key answers; a click, a 9P write or a script still closes it at once, rows going up as before. - A prompt (Save's path, a search, a pipe) is drawn in the tagline's pitch like the other notices instead of spread one glyph to a body cell, and in the SDL shell every notice chip is ruled off from the body like a tagline. - Answer 9P on the connection's task instead of the editor's loop. The core is single-threaded and `pardes.turn` says whose turn it is with it: the editor's by default, given up while it waits for input and while a step of it is out in a syscall, and taken by a connection task to answer a request. A request that would change a pane while a step is out parks in the engine and is retried when the editor rests. So a session can open, read and save its own tree through a mount -- which used to hang it outright, and was then refused by name -- and the name-based refusal, the in-process routing of a mount of oneself and the mailbox that shipped every request to the editor's thread are gone. - A notice wider than its pane keeps its tail rather than its head: the end of a message is the file name or the reason. - Give pty children their own terminal identity. `TERM`, `COLORTERM` and `TERM_PROGRAM` are now written by pardes rather than inherited from whatever launched it, and the child is exec'd with that environment. A macOS `.app` launch supplies no `TERM` at all, so until now `clear` failed, nothing painted a colour, and every cursor-addressing program fell back to printing lines. - Answer tty occupancy on macOS. `ttyTaken` had no darwin implementation and said `false` for every pane, which made `Pardes.takesCommandLine` permanently true: a bare Escape in a raw terminal ran the `Last` builtin instead of reaching the child, and `Exec` always believed the pane sat at its prompt. libproc answers it from the tty's foreground process group, and the occupancy suite now runs on both platforms instead of only Linux. - Flatten the 9P control tree: `self/` is gone, so `/index`, `/pane//`, `/screen` and `/os` sit at the root. `/new` is one clone file (reading it makes a pane and answers its serial), `/ctl` takes `look TEXT` and `exec TEXT` in the editor's own command language and answers the serials a command touched, `/log` streams pane events, and each pane gains `name` and `sel` (replacing `rdsel`/`wrsel`); `cons`, `new/` and the `name`, `put`, `del` and `delete` ctl verbs are gone in favour of `name` and `exec`. Stats carry real lengths, modes and mtimes. Sources are served under `/src` only with `-Dembed-sources=true`. The tree code moved from `src/fs.zig` to `src/ninep/`. ## 0.3 - Add pinned tree-sitter declaration context, with optional compact tagline styling and separators between discontinuous source lines. - Configure search and LSP result context with `LocationsConfig`. Results use aligned, muted context and navigate actual matches with `n` and `N`. - Follow PDF links through Look, with a link cursor and a choice of destinations when a target also names a valid Look location. - Remember tagline cursors, distinguish filenames and terminal commands with theme colors, and keep compact tag text, pointer hits and anchors aligned. - Reorder columns with aligned, distinctly colored drag grips. Grips leave a small gap before command text; GUI pane indicators center using font metrics. - Cycle pane input modes with `Mode`, retain it in terminal tags, and handle keypad Enter in native and attached terminal sessions. - Preserve selections through jump history, support mouse thumb navigation, align click and insertion cursors, and mirror terminal mouse selections. - Make output location highlighting opt-in and ignore slash-only comment markers during general Look navigation. - Use cloud9 for 9P sessions and transports and add Linux mounted terminal sessions with raw terminal key forwarding. - Detached sessions now use protocol version 6. Update the server and attached clients together. ## 0.2 - A panic is written down somewhere it survives. Every crash this program has ever had went to stderr and nowhere else, and stderr is the one place it cannot keep anything: in the TTY shell stderr IS the screen, so the trace lands on the grid the terminal is being reset out of and the next `clear` takes it; the SDL and AppKit shells have no terminal at all; a `--detach` session's goes wherever its launcher left it, which is usually nowhere. So the message and the frames behind it are now appended to `crashes` beside the `init` file, under one line naming the build that produced them — version, commit, UTC timestamp, os-arch and pid — which is exactly the set a bug report needs and the set a reporter cannot be asked to reconstruct after the fact. `src/crash.zig` runs inside the panic handler, so it takes no lock of this program's, allocates nothing of its own, and every failure is swallowed: a crash file that could not be written must not become the crash, and stderr still gets its own copy either way. The file carries NO STACK TRACE, and that is measured rather than chosen: `writeCurrentStackTrace` called from a panic handler *before* `defaultPanic` wedges the process at 0% CPU, and `captureCurrentStackTrace` — which looks like the safe half of it — takes `SelfInfo`'s rwlock exclusively on its first call, so a panic inside the walk leaves that lock held and `defaultPanic` waits on it for the life of the process. A crash that becomes a hang is worse than the crash. What makes `defaultPanic` itself survive that is its private `panic_stage`, reachable from nowhere outside `std.debug`, so the frames stay on stderr where they already work. ONE record per process, because the nested panic std's trace printer raises comes back through the handler: the first version of this wrote the real message and then "reached unreachable code" underneath it, which is the panic handler's own second `vaxis.recover()` double-closing the tty — `recover()` never cleared the global saying there was one. That call is guarded now too, in both the panic and the segfault handler, which is a fix older than this file. The AppKit shell got a panic handler of its own in the process: the macOS build roots at `macos.zig`, so the one in `main.zig` had never run there — in the shell with the least useful stderr of the four. - A big screen can attach to a detached session. The wire's grid ceiling is 512x128 and no frontend ever clamped to it — the hello carried the window raw — so a 4K display at a small font, which is already past 128 rows, had its geometry refused by the session's decoder as `BadValue`. That path answers with `close(.protocol)` and no `refuse` behind it, so the frontend reported the one thing a bare hangup can mean: "that session hung up on the connect", at a session with all 32 slots free. `client.zig` now asks for the largest grid the protocol carries, which is what its own GEOMETRY note already promises a frontend gets — the session is drawn at its own size in the corner of a bigger window, exactly as when another frontend is the smaller one. A zero geometry is dropped rather than clamped, because the session grid is the smallest common one and a frontend reporting 1 would collapse everybody else: `TIOCGWINSZ` answers 0x0 during a teardown and the tty shell forwarded it, so that was the same mute hangup by another route. The clamp alone would have replaced one bug with a worse one: `max_cols * max_rows` is 65536 and a run's length prefix is a `u16`, so the single grid legal at both bounds is the one grid whose full frame — and an attach always produces a full frame — cannot be described by one run. It panicked on the `@intCast` in a safe build and was illegal behaviour in a fast one. The encoder splits the run instead, which `frameBound` had already paid for, and the protocol version is bumped to 2: the geometry a v2 frontend now asks for is one a v1 daemon panics encoding, so a mixed pair — `zig build` replacing the binary under a running session — meets a `Refusal.version` instead of losing every pane shell the daemon owns. - `pardes nosuchfile` opens an editor. It used to return `BadArgs` out of `main`, which std prints as `error: BadArgs` with a return trace under it: indistinguishable from a crash, for a typo, and it left the human with no editor at all. A launch that names nothing now boots one `+Errors` pane filling the window — acme's own vocabulary for output that came from the program rather than from a word somebody clicked — saying `file or directory not found` and the argument AS TYPED, in the directory it was typed in — an output pane's directory is where a `Grep` from it walks, where its `Newtty` spawns and what its `Save` prefills, so a pane rooted at `""` would have pointed all three at `/`. A typo INSIDE pardes is refused by that shell in one line instead: the hand-off block that keeps a pardes from stacking a second full-screen UI inside a pane of the first says in its own comment that a word naming nothing must not get through, and an `+Errors` boot would have made a typo the one input that did. A `chdir` that fails on a directory that really is one is still `BadArgs`: that is a permission problem, not a typo, and the two want different answers. - The macOS trackpad's two verbs trade places: a two-finger click is Exec and a deep press is Look. Exec is what a hand spends a session doing, so it takes the gesture that costs nothing, and the verb that goes somewhere is worth one deliberate push past the click. Three fingers stay Exec, and that is the hardware rather than a gesture left unused: macOS's secondary click is "click or tap with two or more fingers", so a three-finger click arrives as `rightMouseDown` exactly like a two-finger one, and the resting count is the only thing that keeps either of them off Look. `pressureChange` still releases the click in flight before the new button goes out, which now steps around acme's 1-3 chord (Paste) where it used to step around 1-2 (Cut). Look by finger count is gone, so a Mac with "Force Click and haptic feedback" off — or a trackpad with no force sensor — reaches it through a real mouse's right button, the `Look` builtin, or Enter on the word. `trackpad.snap` asserts all of it without a trackpad, because `NSTouch` and the pressure stages have no public constructors and `Trackpad` is therefore pure policy a script can name finger counts to; the swap left every golden screen byte-identical under the other gesture, which is what shows the two paths were exchanged and nothing else moved. - A filtered terminal costs what an unfiltered one does. `Filter`'s second stage asked `RGB.contrast` for every cell it painted, and that call ends in `std.math.pow` six times over — a libm round trip per cell, per frame, to re-derive a ratio against a background that had not moved. The indexed path takes a `u8`, so all 256 of its answers are now enumerated once per pass, after the two default roles are fixed and before the first cell is read; what a cell names is an array index into them. Only truecolour, whose 16.7M inputs cannot be tabulated, still reduces, and the direct-mapped RGB cache is what keeps that cheap. Tracy over the AppKit shell, ReleaseFast, 190x56: the recolour pass falls 3.09 ms to 0.130 ms and the whole frame 3.37 ms to 0.299 ms. The luminance table is comptime-evaluated from ghostty's own expression and a test pins it to `RGB.luminance` and `RGB.contrast` exactly, across every channel value and all 65 536 palette pairs, because the filter's decision is a threshold comparison where one ULP is a different colour on screen. - A pardes launched from the Dock finds the same programs a pardes launched from a terminal does. LaunchServices hands a bundle launchd's own environment, whose `PATH` is `/usr/bin:/bin:/usr/sbin:/sbin`, and every pty shell, `|` filter and language server inherited it — so `yazi` in `/opt/homebrew/bin` was missing in the app and present in the same build run from a shell, which reads as "the Dock build is broken". `host_io.Shell` composes `/etc/paths` then `/etc/paths.d/*` in the order `path_helper` reads them and adopts the result before the first fork in all four native hosts. It appends, so an inherited entry is never demoted and a configured `PATH` is left byte-for-byte alone: the rule is that only a `PATH` nobody configured gets repaired. - Language servers other than ZLS start on macOS. The protocol client opened its control socket with `SOCK.CLOEXEC`, which Zig defines for Linux and which Darwin answers with `EPROTONOSUPPORT` — so `socketpair` failed before any fork, every spec in the table reported "no server", and rust-analyzer, clangd and gopls were unreachable in every macOS build. It is a plain socket plus `fcntl(FD_CLOEXEC)` now, the route `fuse.zig` and `nested.zig` already used. Verified end to end against a 200-crate Rust workspace: rust-analyzer indexes, and `gd`, `document_symbols` and `hover` answer. - `SPC l i` reports what the language backends are doing in the macOS app, and `|` filters a selection there. Both effects were unimplemented host methods — `lsp` and `pipe` were absent from the AppKit vtable, so the core answered its own empty answer and the panel rendered blank while a sort filter silently did nothing. The snapshot-plus-worker body each shell had its own copy of is one module (`src/lsp_host.zig`), and the in-flight pipe table is `selection_pipe.Tasks`; all three native shells share both. Unsolicited server news ("rust-analyzer indexing 45%") reaches the message row because the status sink is registered rather than merely defined. - Animation runs at the speed it claims. `pardes_animation_tick` advanced one scene frame per callback and reported time as `frame_count / 60`, so animation time was a count of how often the callback happened rather than how much time had passed — and the AppKit chain re-armed `asyncAfter(.now() + 0.016)` only AFTER the previous frame's work, making the real period 16 ms plus all of it. Motion ran at roughly three quarters of wall clock, unevenly. The tick measures elapsed monotonic time and spends it in whole 16 ms steps, banking the remainder, so a late callback advances two frames instead of stretching one; the arithmetic is a pure function with its own tests, no display attached. On macOS 14 and later the run is driven by one `CADisplayLink` phase-locked to vsync instead of a chain rebuilt per frame. - A filtered terminal's colours are mapped in two stages, and the second one keeps text off the page. `Filter` reduces every colour to its nearest canonical xterm key and reads that key out of ghostty-vt's theme-derived 256-colour projection — a comparison of RGB triples, which knows about hue and nothing about the background. The projection is generated FROM the default foreground and background, so its cube corners are those two anchors, and the nearest key to a truecolour extreme was therefore the background itself: `\x1b[38;2;255;255;255m` on acme's `#ffffea` paper resolved to `#ffffea`, a WCAG ratio of 1.000, text painted the colour of the page under it. Every curated theme owned such a key — 231 on the light one, ANSI black on both dark ones, which is a bare `\x1b[30m`. The two default roles are now mapped first and named as the anchors they are, and every other FOREGROUND has to clear `config.tty_filter_min_contrast` (1.5) against the mapped background; one that cannot is not mapped at all, and takes whichever anchor is still visible there. Backgrounds are exempt, because a background is the page the floor is measured against. Measured across the curated three the floor refuses 12, 16 and 7 of 256 keys, where a WCAG body-text 4.5 would refuse 61, 154 and 91 and flatten the palette. ## 0.1 - Pane taglines sit where they do in the SDL window. The macOS shell had its own copy of the band geometry and it only ever centred the shorter band in its body-sized row, which is the exact case `gui_topbar_pane_border_px` exists to prevent: the topbar's unused half-band and the first pane tag's unused half-band meet, and the window background shows through the seam — 4 physical pixels at the default 82%, 10 at 50%, 14 at 30% on a 20-pixel cell, so it grew as the tagline face shrank. The rule moved into the core and both pixel hosts now call it: row zero bottom-aligned, the first pane-tag row top-aligned, the two joined by the themed rule, rows between centred, and a `Tagbottom` band on the last row flush with the window edge with the sub-cell strip beneath it painted in the band's own colour instead of the page's. - Files reload themselves on macOS. The tty shell, the SDL window and the detached daemon all watched with `inotify` and therefore watched nothing off Linux: an edit made outside pardes never reached the pane, and a PDF replaced on disk kept rendering the old inode. They now share one mark/reconcile transaction over two kernels — `inotify`, or a `kqueue` whose queue the hosts wait on and poll exactly as they did the old descriptor. A macOS mark is two filters, because a kqueue directory filter reports its entries changing and never a write to a file already inside it: the parent follows rename-over saves, the file catches in-place writes, and the file filter is re-armed once a rename-over has moved the inode. That is the same pair the AppKit host's DispatchSources already used for the same reason. - Snapshot captures are deltas against the previous capture in the same script, and a scripted click may name a word (`press middle @Del 2`, `@Del#2` for the second pane on a row, `@Save-2` for a column beside one) instead of a screen column. Adding one builtin word to a tagline used to rewrite 78 goldens (3,758 lines) and silently move seven scripts onto the wrong word: `tutor.snap` had been clicking `Grep` and asserting a tutor pane, `tagbottomimage.snap` clicked 176 columns from the `Del` it claimed to execute. Seven tests regained their subject, the corpus dropped from 16,700 lines to 5,722, and the same edit now moves 337 lines. - `zig build snap -- --update` regenerates in parallel at the widest probe settings and then verifies every golden it just wrote, instead of capturing once, serially, and blessing whatever it saw: 77s to 41s, and a capture that does not reproduce is reported rather than committed. - `zig build unit-test -Dtest-filter=` runs one test. The whole binary is 14s, so a one-line change used to cost the full run; the test runner's own `--test-filter` was silently dropped by the build. - A snapshot script covers the `Save` prompt for the first time: armed on a `+New` scratch it draws on the message row, prefilled with the pane's own directory, and submitting it turns the scratch into the file it names. - `pardes --fs` serves acme's control filesystem over Linux FUSE: a directory per pane holding `addr`, `body`, `ctl`, `data`, `errors`, `event`, `rdsel`, `tag`, `wrsel`, `xdata`, plus `index`, `cons` and `new/` at the root. A program that opens those files is an editor extension — no plugin API, no interpreter. `--fs=` names the mount point instead of deriving one under `$XDG_RUNTIME_DIR`; every pane shell gets `PARDES_FS` and `PARDES_PANE`. See docs/acme-fs.md and examples/acmefs/. - While a script holds a pane's `event` file open, buttons 2 and 3 in that pane are REPORTED to it instead of performed, so the words in that pane's tag are the script's commands (acme's extension model). Keyboard Enter/Tab still execute, so a scripted pane stays editable. - Writing an event record back (`origin type q0 q1`) makes pardes perform the Look or Exec it names — the same door acme has always had, and the reason the mount is 0700. - Version tracking: the compiled version is embedded from build.zig.zon and shown by Changelog. - New builtins: Changelog, Newtty, and Joincol. - Markdown tree-sitter highlighting, including nested fenced code blocks. - `.patch` and `.diff` files are highlighted. - Coloring dispatch keys off the buffer title. - Opening a new file only splits into a new column when both panes would be at least 100 columns wide. - Terminal ANSI colors render only in tty mode; normal mode is a plain editing view, so an edit can never shift a shell row's colour. - `New` and `Newcol` open an empty scratch buffer instead of a shell or a temporary file. Its directory is inherited from the pane it was opened from. - `Save` on a scratch asks for a path, prefilled with that inherited directory, and the buffer becomes an ordinary file once written. - `Save` reaches every pane that holds text of its own and leads its tagline: a terminal ("Save New Newtty Del Filter") and an output buffer like `+Search` ("Save New Newtty Del") as well as a file. Images and PDFs keep the plain tail — their bytes on disk already are what they are. - `Save` takes the path as an ARGUMENT (`Save notes.txt`, or a selection chorded onto the word) and asks for one only when it was not given, the way Find and Grep ask for a pattern. A relative path resolves against the pane's own directory, with `.` and `..` normalized; a path that cannot be made absolute is refused rather than written next to the process. - Writing to a path never rewrites the pane: a terminal stays a terminal, an output buffer keeps its rows and its place in the n/N ring, and an open file keeps the file it has, so `Save ` is a copy and not a rename. The scratch `+New` is still the one exception, becoming the file it names. - A dropped pane's memory outlives its frame, so a pane pointer taken during that frame stays valid until the next one repairs it. - The core owns the event loop. Each platform is now a `Host` of optional function pointers; an unimplemented method falls back to an in-process default, so a host with no methods at all is still a complete pardes. - Ctrl-V and Ctrl-Shift-V paste in a tty pane: the default register and the system clipboard, typed at the program the way a terminal emulator pastes (bracketed under mode 2004). A paste arriving from the desktop reaches the shell instead of an edit buffer the pty cannot see. - The default filesystem is pardes's own source, embedded from an allowlist (src/source_manifest.zig): what a host with no file method reads, and what the browser has always read. It replaced a build-time generator that embedded every tracked .zig file. - `zig build run-isolated` builds and runs `pardes-isolate`: the same source with one comptime option, whose host supplies only the terminal. The filesystem is not disabled in it, it is absent — the libc paths compile away. - Typst highlighting: headings, `*bold*` spans, raw/code blocks and inline raw spans, and `#`-prefixed function calls (including the sigil itself). - Fenced Typst raw blocks with a language tag (e.g. ```` ```zig ````) get that language's grammar injected, the same treatment markdown fenced blocks already had. - Markdown bold, italics and inline code spans are highlighted: pardes now builds the markdown inline grammar, a second parser off the same dependency, and re-parses the block grammar's `inline` nodes with it. Only the block grammar was built before, so emphasis had no nodes to match. - Escape in a PDF pane is the document's own cancel: it drops the mouse selection and the search highlight overlay and stays where you were reading. It used to run `Last`, which moved focus out of the pane and left every following key going somewhere else. `Shift-Esc` is now the hop out — the same chord that leaves a raw tty. - `gj`/`gk` move one VISUAL line, following the automatic breaks of a soft-wrapped body and keeping the goal column inside the row. `j`/`k` are unchanged: whole file lines. With `Wrap` off a line is one visual row and the two pairs are the same motion.