summaryrefslogtreecommitdiff
path: root/build.zig.zon
Commit message (Collapse)AuthorAge
* A shell that exits under a run answers exit N, and the log says exit before delGabriel Schneider2 days
| | | | | | | | | | | | A dogfood agent ran `exit 3` through pty/run: the reader got ENOENT, and the log went straight to del. The hosts now read a shell's exit status at its pty's end as they do a command's; a run waiting on the line answers `exit 3` with what it printed, the log says `exit <serial> 3` before the pane's `del`, and an open run still stats after its pane is gone (cat fstats its input). Pins cloud9 f35b7ed, whose stat of an open fid names its open. Writes to a gone pane's pty/data, held open or not, fail ENOENT. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Sixteen 9P connections, and the next one is told there is no room instead of ↵Gabriel Schneider2 days
| | | | | | | | being reset Four slots ran out under scripts plus a mount, and the fifth client was closed without a word, which through a mount looks exactly like a dead session. pardes now serves sixteen, and builds against cloud9 09b77cf, whose runner answers the Tversion of a client it cannot seat with an Rerror "too many connections" and tells pardes, which logs `err - 9p: too many connections`. QUIC keeps its own sixteen and still just closes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* pardes builds against cloud9 2a7137c, whose 9ns reads a reply that came in ↵Gabriel Schneider2 days
| | | | | | | | before a hang-up 9ns failed every waiting call as soon as one send met a closed socket, so an answer the server sent just before hanging up could be lost: the Restore write's, when pardes cuts its connections. The docs no longer say the Restore write usually fails through a mount; only an older 9ns could, and the log stays the authority. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Split control messages by scope: the root ctl takes the session's builtins ↵Gabriel Schneider2 days
| | | | | | | | | | | | | | | | | | | | and reads the settings, a pane's ctl its own Every builtin could only be clicked, or written to exec, and the settings could be read only as the Config window's prose. acme keeps window verbs on a window's ctl, and webfs and upas/fs keep session settings on a root ctl. Each builtin now declares its scope (scope = .session; settings are all session, the rest pane), read by the registry. The root /ctl takes session builtins and reads every setting in the words a write takes, so its read written back changes nothing (panel and scene effects now take on/off like the toggles, to make that true); a pane's ctl takes the pane's builtins beside get, lock and unlock. Writes are checked whole and refused in Plan 9's ctl words (unknown control message "X", wrong #args ...), which 9ns now maps to EINVAL (cloud9 re-pinned at a8c7a715). A builtin that would prompt for its argument fails the write instead, and a refusal is answered at once, not after the frame. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Answer a held read by its cloud9 ticket, refuse a second, and say a pane ↵Gabriel Schneider2 days
| | | | | | | | | | | | | | | | | | | | | | | shut down The held read is now kept by the Ticket cloud9's Conn.hold() gives its park and answered through Conn.answerWith(), which makes the answer only while that very park still waits, instead of walking the engine's slots by tag; pardes no longer reaches into the engine for it. A second read on an open whose read is held fails with file in use rather than sitting parked where nothing answers it, and a read that waits with no open record to hold it is logged and asserted on. A read on an event or pty/data open whose pane closed answers acme's "window shut down" (editors/acme/xfid.c:1005). The pane keeps its run's and its lock's open handles, checked through openOf on use, not record indices. Docs: lock from a shell needs a held fd, and a command that clears the screen may read as cut. Needs cloud9 zvuqvnzy (cca47d63), which adds Conn.hold, waiting and answerWith; build.zig.zon still pins 82d8152c until that is pushed and re-pinned. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* pty/run answers with the command's output; pin cloud9 with 9ns concurrency ↵Gabriel Schneider2 days
| | | | | | | | | | | | | | | | | | and E.BUSY The answer is now the header line (exit N, or exit N cut M when only the last 64 KiB were kept, or exit N cut when its start scrolled out of the history), then what the command printed: the screen text between its C and D marks, which the marks handler pins so scrolling keeps them. Also from review: a plain /log open honours its offset until follow is written, so tail -n and less work; whitespace-only run lines are refused. cloud9 is pinned at 82d8152c: 9ns keeps up to 32 requests in flight per mount so a waiting read no longer freezes the rest of it, and E.BUSY replaces the errno pardes spelled locally. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Shape text with HarfBuzz, so a font's programming ligatures draw across ↵Gabriel Schneider2 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | their cells The SDL shell rasterized every cell by its own codepoint, so a font's `->`, `!=` or `<=` ligature never appeared. Text is now shaped with HarfBuzz 11.0.0, built from source like FreeType: a lazy build.zig.zon dependency (the tarball Ghostty pins) compiled only for the gui shell, its FreeType integration linked against the freetype package, so the binary carries no system HarfBuzz or FreeType and a tty build never compiles it. Shaping turns on only the font's ligature features (liga, calt, clig, rlig, rclt); composition, local forms and fractions stay off, so a cell draws either its nominal glyph, as it always did, or part of a ligature. A glyph no lookup of those features covers cannot change, so its cell is resolved alone, and that resolution is cached by codepoint: in a font without ligatures (Adwaita Mono) no cell is ever shaped, and the pipeline draws pixel for pixel as before. A cell whose glyph a ligature could replace is shaped with its word: the cells between spaces that share a decoration and a role. A word ends either side of the cursor, block or bar, so the cell being edited shows its own character (Ghostty's rule), and inside f|i, f|l and s|t, whose typographic ligatures do not belong on a grid (Ghostty again). Colours never end a word: a ligature spans a syntax colour change or a selection edge. A codepoint the primary font lacks shapes in the fallback face that has it. Taglines are not shaped: they are drawn at two pitches, and a strip would not line up in one. HarfBuzz only chooses glyphs; the grid places them. A substituted glyph whose ink reaches over neighbouring cells that draw nothing of their own (the spacer glyphs of Fira Code-style fonts such as Maple Mono, or the cells a merged ligature cluster swallowed) claims them: it is rasterized once as a strip that many cells wide, and each cell samples its own slice, so selection, the cursor and per-cell colours work unchanged. Anything the grid cannot place (several glyphs in one cell, a positioned mark) keeps its nominal glyphs. A frame must not pay for this. A shaped word's atlas slots are cached by its text (checked against the stored text, not just its hash), so a frame costs one lookup per word and a compare for the rest of the word being walked. The atlas is keyed by (face, glyph, role, decoration, lead, span), packed into 64 bits so a lookup hashes one word; ASCII glyph ids are a table, and a face's HarfBuzz state is made when it is first asked about a glyph. Measured with the latency trace, whose F line now also carries the submit time (CPU = submit - present entry), in 10 interleaved rounds against main with an A/A pair, at 160x50 cells: with Adwaita Mono every scenario (idle, scrolling, typing, terminal output) is as fast or 1-3% faster, and the first frame and time to first paint are unchanged. With Maple Mono, ligatures cost the first frame about 0.8 ms (HarfBuzz setup and shaping the screen) and scrolling new text about 1-2%. With it the codepoint raster path is gone: grips look up their glyph and stay centered, since a grip is not text. ui_font_scale_for_height, an identity kept as a "size token", is removed (px is the size everywhere), ui_font_has_glyph becomes ui_font_glyph, and the space slot is simply the first, cleared cell of the atlas rather than a rasterized space. CellStyle.ul gets an explicit u3 tag so a decoration fits in the packed keys. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* Answer 9P on the connection's task, so a session can open its own treeGabriel Schneider2 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The editor's loop was the only thing that could answer a 9P request, which made the editor's own syscalls through a mount of its own tree -- a Look at /mnt/9p/pardes/<me>/anything under a `9ns --mntgen` view, a Save into it -- requests only the blocked loop could serve. The name-based refusal that followed (ownMountSuffix) and the in-process routing of a mount of oneself (Client.sameSession) were patches over that, and both are gone, with the mailbox that shipped every request to the editor's thread. One rule replaces them, `pardes.turn`: the core is single-threaded, the editor's thread has the turn by default and gives it up in two kinds of gap -- while it waits for input and while a step of it is out in a host syscall -- and a cloud9 connection task takes it in those gaps to answer. `out` counts the steps that are out, from any thread: while one is, the core reads consistently but that step still holds pointers into it, so a request that would change a pane (a write, a truncation, an rmdir) is parked in the engine and retried when the turn is next given up with nothing out, and the editor's own wake waits for the count to reach zero. It is never a write of its own that a step waits on out there -- writes come from a shell performing a save between steps -- so a parked request is never the syscall's own, and making a pane or rendering a screen need not park: every yield sits before its step's mutation, so the layout and the surface are whole under it. A changing request that queued effects is answered once the editor has performed them (`echo Save > exec` returns with the file written, as acme's `put` does), and it settles the way a step does, because without that a /log reader waited for the user's next keystroke. Every host syscall on a user path has to give the turn up, not fs.zig's alone: the first end-to-end run hung in `inotify_add_watch` performing the new pane's watch effect. PDFs and images are read whole at open, so no draw goes out into the host. The core's allocator takes its fixed buffer through the lock-free interface, since a connection task allocates while the editor's thread is out in a syscall that allocates too. A Restore puts the replacement in first and releases every task waiting on the old core. cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a remove on `again`, not only reads and writes, and answers a parked job whose fid was clunked without asking the backend. Verified: test/selfmount.py runs the editor under `9ns --mntgen` and Looks at, reads and Saves its own tree through the mount; a unit test pins that a change parks while the editor is out mid-step and lands when it rests, while a read is answered in the window. 9P over the Unix socket against a tty session, same machine, Debug builds: a read of /index 278us -> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us; the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells). Also from the reviews: a notice chip over an image or PDF pane was painted out by the picture drawn after the cells, so pictures give up the rows; in the GUI a tree-sitter context band painted over the chip, so body layers are emitted first; a message is one row of printable text, its 256-byte cut never leaves half a glyph, and one wider than its pane keeps its tail (the file name, the reason) rather than its head. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Advertise the editor in the posted-9P registryGabriel Schneider2 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | pardes spoke 9P and could be mounted, but only by naming its socket: $XDG_RUNTIME_DIR/pardes-9p-<name>.sock sits one directory above the registry and nothing could find it. Now a listening editor advertises itself at $XDG_RUNTIME_DIR/9p/pardes/<name>, a symlink to the socket it already binds. One directory for the program, one entry per editor — the layout zmx posts its sessions under — so several editors group rather than crowd the registry root. The socket does not move: adopting the registry only advertises. Only the runtime-directory socket posts, so an instance on the ~/.local/state fallback stays out of the user's registry, the way a private ZMX_DIR does for zmx. Stopping unposts, and only while the entry is still ours, so a name another editor has since claimed is never unlinked. The cloud9 pin moves to 9c4d668c for cloud9.post's path helpers. Serving is the whole of it. Consuming the registry is 9ns's job: it mounts the lot at /mnt/9p and an interactive fish already self-wraps in one, so a pardes started from a terminal reads /mnt/9p/harness/... with the same code that reads any other path. Two drafts that taught `resolve` to dial the registry itself were reverted — one duplicated 9ns for no gain, the other reinterpreted relative dials, which are a feature. `resolve` is byte-identical to what it was, and no dial that worked changes meaning. What pardes still does not do, and why, is in docs/cloud9.md: it binds its own socket rather than posting through cloud9.post, because post claims flat names only — legalName rejects '/', and claimName derives its lock directory by stripping "/9p" — so a name inside a subdirectory cannot go through it. zmx hand-rolls the same symlink for the same reason. Unifying them means teaching post a group, which is a change to adversarially-hardened code rather than a rename. docs/divergences.md records what this bookmark move leaves beside it: the editor line rruwvuzm (~1300 lines, forked at 01104e7c, still on the old cloud9 pin), the other bookmarks, and two failures that are not this change — fs-test's syntax-highlighting assertion, which fails identically on a clean main, and pardes not starting headless, which is why this is covered by 9p-io-test rather than by running the editor. Tests: 11/11 9p-io-test, including a listener that posts on start and unposts on stop; 31/31 unit-test.
* Serve Unix and TCP 9P through cloud9.serve's std.Io runnerGabriel Schneider2 days
| | | | | | | | | | | The hand-written poll loop for Unix/TCP listeners is replaced by cloud9.serve.Runner; requests are queued to the editor thread, which answers them under the connection lock on each frame and retries parked reads as before. QUIC keeps the poll path (its adapter is fd based). The detached server no longer loses a wake that lands between frames. The firmware path keeps driving the engine with push/step. cloud9 re-pinned. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Use cloud9's file-server engine instead of the private copyGabriel Schneider2 days
| | | | | | | | | | | src/9p.zig shrinks to an 88-line alias: the engine and the backend contract (Req, Reply/ReplyWith, Op, Status, Attr, E, error strings) now come from cloud9.fs. The editor's limits become cloud9.fs.Options values; the ESP32-P4 board backend drops its own copies of the contract types. No behaviour change; fs-bench still allocates nothing per request. cloud9 re-pinned to the commit that carries the engine. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Fetch cloud9 from sourcehut, and correct the citations into the board toolchainGabriel Schneider2 days
|
* Release Pardes 0.3 and stamp the macOS package versionGabriel Schneider2 days
|
* 9p: use cloud9 protocol sessions and transportsGabriel Schneider2026-09-15
|
* hosts: the effects three shells kept a copy of become one, and the mac's own ↵Gabriel Schneider2026-09-02
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | bugs go with them Nine read-only scouts compared every host-side concern across `src/macos.zig`, `src/tty/tty.zig`, `src/gui/gui.zig` and `src/detached/server.zig`. What they found was not a style problem: each duplicated body had drifted, and in every case the drift WAS a bug the users of that shell could see. So the fixes and the deduplication are the same change. **One PATH, adopted before the first fork.** LaunchServices hands a bundle launchd's environment, whose `PATH` is `/usr/bin:/bin:/usr/sbin:/sbin`. Every pty shell, `|` filter and language server the app forked inherited it, so `yazi` in `/opt/homebrew/bin` was absent from a Dock launch and present in the identical binary run from a terminal — the "it worked briefly" window was simply the sessions started from a shell. `shell_bin.adoptSystemPath` composes `/etc/paths` then `/etc/paths.d/*` in the order `path_helper` reads them, deduplicating on first occurrence, and runs once at startup in all four native hosts. It APPENDS: an entry already present keeps its position, so running it over a real session cannot demote a mise shim behind `/usr/bin` and silently change which `node` runs. A `PATH` that was configured is left byte-for-byte alone; only one nobody configured is repaired. `prepareForFork` folds that adoption together with the prompt-rc staging and the `BASH_SILENCE_DEPRECATION_WARNING` setenv the five hand-copied prefork sites had between them — `server.zig` had none of it, which is why every detached pane opened with Apple's zsh banner. **The LSP protocol client never worked on macOS.** It opened its control socket with `libc.SOCK.CLOEXEC`; Zig defines that constant for Linux and Darwin answers `socketpair` with `EPROTONOSUPPORT`, so the call failed before any fork, `ensure` returned `error.NoServer`, and every row in the spec table — rust-analyzer, clangd, gopls — was unreachable in every macOS build. The in-process ZLS backend kept answering, which is what made it read as "only Zig is supported". It is a plain socket plus `fcntl(FD_CLOEXEC)` now, the route `fuse.zig:943` and `nested.zig:95` already took for the same reason. The snapshot suite that covered this path had never run natively on a Mac: the harness targets defaulted to x86_64-linux. **One LSP host worker.** `src/lsp_host.zig` is the snapshot, the worker body and the job lifetime that `tty.zig` and `gui.zig` carried verbatim — `gui.zig` said so in a comment — and that `macos.zig` did not carry at all: `lsp` and `pipe` were absent from its `Host.VTable`, so the core answered its own empty answer, `SPC l i` rendered a blank panel and a `|` filter silently did nothing. All three shells share the module, and the AppKit host implements both effects. Its status sink is now REGISTERED as well as defined, so unsolicited server news reaches the message row instead of nowhere. **The animation clock measures time.** `pardes_animation_tick` advanced one scene frame per callback and published `frame_count / 60`, so scene time was a count of callbacks rather than elapsed seconds — and `AppDelegate` re-armed `asyncAfter(.now() + 0.016)` only after the previous frame's work had finished, making the true period 16 ms plus all of it. Motion ran at about three quarters of wall clock and unevenly. The tick now spends measured monotonic time in whole `frame_ns` steps and banks the remainder, so a late callback advances two frames instead of stretching one; `spendTickTime` is that arithmetic as a pure function with its own tests and no display attached. On macOS 14+ the animating run is one `CADisplayLink` phase-locked to vsync rather than a chain rebuilt after every frame; macOS 13 keeps the old chain. **Three more single definitions.** `panel_animation.paintOrder` is the moving-then-opening-then-closing composite order as a rule the core applies once in `Pardes.render` — `macos.zig` was re-sorting an already-sorted list. `selection_pipe.Tasks` is the bounded in-flight pipe table `tty.zig` and `gui.zig` each declared. `boxContains` was a fourth copy of the half-open cell test and is now an alias of `Box.contains`. **A filtered terminal stops asking libm per cell.** `Filter`'s legibility stage called `RGB.contrast` for every painted cell, and that ends in `std.math.pow` up to six times, re-deriving a ratio against a background that had not moved; the existing memo cache covered the palette reduction beside it and never this. The indexed path's input is a `u8`, so all 256 answers are enumerated once per pass — after the default roles are fixed, before the first cell is read — and what a cell names becomes an array index. Only truecolour still reduces. ReleaseFast, 190x56, Tracy: recolour 3.09 ms -> 0.130 ms, frame 3.37 ms -> 0.299 ms. The comptime luminance table is pinned to `RGB.luminance` and `RGB.contrast` by exact-equality test over every channel value and all 65 536 palette pairs, because the decision is a threshold comparison where one ULP is a different colour. A `filterInit` Tracy zone records the part that is still per-pass: 2.9 us warm against a 117 us pass, which is the measurement that says not to cache it across frames. Released as 0.0.2. `build.zig.zon` carries the version into `pardes --version` and into the `Changelog` pane through `@embedFile`, so the entries above open a `## 0.0.2` section and `## 0.0.1` closes with the tagline work of the parent commit. Two bugs here were mine, caught by review rather than by me: a double free in the macOS pipe drain arm (`Msg.free` already owns the response) that segfaulted the app on the first `|`, and a proposed `getRowAndCell` optimisation that targeted 2 of 43 draw samples while the contrast math beside it took 12 — and would not have compiled. The profile that justified it was a Debug build, which `build.zig:1160` already documents as ~5x slower than release. Native and -Dplatform=macos suites: 0 failures. All targets build with Tracy on and off; the shipped release binary contains no `___tracy_emit_zone_begin`. App reinstalled, signature verified, dmg regenerated, launched with 0 crash reports; installed binaries verified byte-identical to a fresh build.
* build: pardes builds without the ESP32-P4 toolchain checkout beside itGabriel Schneider2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `build.zig.zon` named `../05-zig-p4` as a path dependency, and `build.zig` `@import`ed it inside `if (-Desp32p4-firmware)`. But `@import` in a build script is resolved when the SCRIPT is compiled, not when the branch that needs it is taken -- so naming the package at all meant anyone without that sibling checkout could not build pardes AT ALL. Not the firmware: the terminal shell, the SDL shell, the tests. `zig build` failed with build.zig:1073: error: no module named 'zig_p4' available within module 'root.@build' from a line inside an `if` that was false. Neither escape hatch works for a PATH dependency, and both were tried rather than assumed. `.lazy = true` is about FETCHING; a path dep whose directory is absent is generated as a package with no `build.zig` rather than one marked unavailable, so `b.lazyImport` -- which exists for exactly this and is what the standard library says is to `@import` what `lazyDependency` is to `dependency` -- reaches a `@compileError` instead of returning null. Making it a fetched dependency instead is not available either: the toolchain has no remote. So the duplicate goes. That build tree's firmware block linked an image the toolchain repository already knows how to link -- its own build.zig has `-Dpardes`, `-Dapp=<root>` and `-Dpardes-obj=<path>`, and its comments record having learned this same lesson from the other direction, where nesting pardes's ~30-package graph under it broke every build there. The object is the seam: it crosses by PATH and never by package, and each repository builds what it owns the pieces of. zig build -Dplatform=esp32p4 # here, no toolchain needed zig build -Dpardes # there, the console image zig build -Dpardes -Dapp=<pardes>/src/esp32p4_9p.zig # there, the 9P image For the second and third to work with no module map, `src/board9p.zig` and the 9P firmware root now reach the codec by PATH instead of through a named `ninep` module that only pardes's own build.zig knew to inject -- which is also why the root moved from `src/esp32p4/nine.zig` up to `src/esp32p4_9p.zig`, beside `src/esp32p4.zig`: a path import may not escape its module's own directory. Both files are now self-contained, and `zig test src/board9p.zig` works with no flags. `-Desp32p4-port`, `-Desp32p4-prof` and `-Desp32p4-cpu-mhz` go with the block. An option this build cannot honour is worse than no option, because it accepts the flag and then ignores it; all three are spelled the same way in the toolchain. Verified by moving ../05-zig-p4 out of the way: `zig build`, `zig build -Dplatform=esp32p4` and `zig build unit-test` all pass without it. With it back, the toolchain still links both images -- console 812,720 B, 9P 88,096 B. next-steps.txt gains the six features the 9P chain shipped.
* One core behind N frontends, the board's own runner moved in, and every ↵Gabriel Schneider2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | board cap on one screen ## The wire is the effect stream, not a new protocol `pardes --detach` leaves a core running with no terminal; `pardes --attach` is a frontend that owns a terminal and a socket and nothing else. N frontends on one core all look at the same screen — `screen -x`, not N sessions. The codec (`src/detached/wire.zig`) carries exactly one `Event` or one `Host.VTable` call per message. That is not a coincidence and it is why there is no third vocabulary to keep in step: the core's IO seam was already a struct of function pointers with plain-data arguments, so a socket is a legal implementation of it. `nested.zig`'s socket could not be reused — it carries a builtin command line, and a command line cannot carry a frame. ARCHITECTURE-NEUTRAL on purpose, not as decoration. The frontend on the far end may be riscv32-freestanding on the ESP32-P4 while the core is x86_64 Linux, so every field is an explicit little-endian fixed width and no message is a blit of a native struct. A protocol that only works between two builds of the same compiler would have thrown away the one frontend that motivated it. ## The board comes in; its toolchain stays out `src/p4.zig` becomes `src/esp32p4.zig`, and the pardes half of `../05-zig-p4` — the vaxis-over- serial runner, the UART editor terminal, the keystroke rescue ring, the on-die test suite — moves into `src/esp32p4/`. `build.zig.zon` gains `.zig_p4 = .{ .path = "../05-zig-p4" }`, so `zig build -Dplatform=esp32p4 -Desp32p4-firmware` builds, flashes, monitors and self-tests the board from this repo's `build.zig`. The DIVISION is the point. What moved is what only pardes wants: the runner that drives a pardes core over a serial line. What stayed is everything a second project would also want — the HAL, the register/radio/oracle layers, the linker script, `_start`. `zig_p4` declares no dependencies of its own and its `build()` early-returns when it is not the root package, so this costs the package graph exactly zero packages and the editor's own builds nothing at all. ## limits.zig: nine forgettable places become one budget Nine `platform == .esp32p4` capacity tests lived in nine files. They were never nine decisions — they are ONE decision, how much memory this build may spend, taken nine times where no reader could see the total. `src/limits.zig` puts the whole budget on one screen with every cap named against what it is measured against, derived from two booleans. The payoff is testability on a machine that is not the board: the caps are ordinary comptime values, so a host build can be compiled against the board's numbers and the parking, eviction and clamping paths a 240 KiB core takes get exercised by the normal test suite instead of only over a UART. ## A bare `zig build` `zig build` with no arguments now builds the tty and GUI binaries and installs them into `~/.local/bin`, and says so once on stdout with the flag that overrides it. The old default built one binary into `zig-out` — a path nothing on a `PATH` ever looks at, which made "build it" and "use it" two different commands for no reason.
* host: the core owns the event loop; every platform becomes a vtable of ↵Gabriel Schneider2026-08-25
| | | | optional methods
* modal + file_pane: rework editing math and pane behavior, config/syntax ↵Gabriel Schneider2026-08-18
| | | | additions, unit tests
* ghostty: bump pin to 82e53e3f and fix tab/virtual-column vt handlingGabriel Schneider2026-08-10
| | | | | | | - build.zig.zon: bump ghostty ba38b493 -> 82e53e3f (translate-c backport, fixes 404 on cold cache), update hash - src/pardes.zig: adapt Terminal.init/resize to new std.Io signatures; fix display-column conversion for tabs and clicks past EOL (fileRawDisplayCol/fileRawAtDisplay) - test/e2e_harness.zig: adapt to new ghostty_vt signatures - snap suite 86/86 green
* Better text renderingGabriel Schneider2026-08-10
|
* tty/image: big harness + golden coverage passGabriel Schneider2026-08-10
|
* multiple cursors, regex selection, and Ctrl-c commentsGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The primary cursor stays exactly where it was — cur_row/cur_col plus vsel — and sels[] holds helix's OTHER ranges. That split is why nothing moved at one cursor: with nsel == 0 not one line of the existing motion, operator, render or mouse code takes a different branch, which is what protects 800 differential cases and 67 goldens. paneRanges/setPaneRanges are the whole list; setPaneRanges IS helix's Selection::new (min width 1, sorted, overlaps merged, primary follows its range through a merge). An ordinary key runs the single-selection handler once per range, visited last-first so an edit never disturbs a range still waiting, and each finished pass is remembered as a distance from the END of the text, which an earlier edit cannot move — helix's change mapping without a change map. pushUndo fires once per keystroke, yanks accumulate, and a builtin acts from the primary and stops the replay, which also closes the use-after-free window if it frees the pane. s and S reuse the / prompt wholesale rather than growing a second one: the pattern is typed into the tag tail, and every keystroke re-runs the match from the selection the prompt opened on, so the preview is live and Esc is just the empty pattern. mvzr does runtime patterns — a bytecode VM in a fixed-size struct with no allocator — with 64 ops and 8 char classes per pattern, no case-insensitive flag (helix's smart case is done by folding a scratch copy), no captures, no multi-line anchors. The last two are the two waivers. Ctrl-c is a whole-list key and not a per-cursor replay, because helix decides comment-vs-uncomment ONCE for the whole selection; replaying it would take that decision n times. Comment tokens are a table in config.zig keyed on the same extension syntax.zig picks grammars by. Found and fixed a pre-existing single-cursor bug on the way: la<bs><esc> left the cursor one cell before where the append began. helix's restore_cursor can never walk past the origin; ours backed up unconditionally. hxdiff was green before AND after — the old one-selection contract could not see it. hxdiff 360 -> 481 cases, hxparity 440 -> 561, all goldens from real helix; the harness contract now reports every range and its primary, omitted when there is one, so 359 of the 360 old goldens are byte-identical. The one that moved is o-count: helix's 2o really does leave two cursors and could not say so before.
* lsp backend: ZLS as an in-process libraryGabriel Schneider2026-08-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The seam's `query` now calls ZLS's analyser directly, on the worker thread, in this process. There is no zls binary, no subprocess, no JSON-RPC, no `initialize` handshake and no `Server` — `gd` is a function call whose answer comes back as rows. ZLS's build.zig already publishes its guts as an importable module (`b.modules.put("zls", ...)`), so this is a path dependency on the local 0.16.x checkout plus one new file, `src/lsp_zls.zig`. Construction is ZLS's own (tests/analysis_check.zig does exactly this): InternPool.init -> DiagnosticsCollection -> DocumentStore struct literal -> Analyser.init. `zig_exe_path` is null on purpose — shelling out to the compiler is the thing this backend exists to avoid — and `zig_lib_dir` is baked in from `b.graph.zig_lib_directory`, so `gd` on `std.mem.count` opens the same mem.zig the compiler used, with ZIG_LIB_DIR overriding at runtime. Offsets are `.@"utf-8"`, not ZLS's utf-16 default: `+Search` rows are byte columns and we are not on a wire. Seventeen probes, seventeen answering, no false claims, 5.9 MiB peak RSS. The features that were already Server-free are calls (hover, document symbols, code actions); the ones welded to `*Server` are reimplemented thin on top of public primitives — goto is gotoHandler minus the protocol, diagnostics is the in-process `std.zig.AstGen` branch of getAstCheckDiagnostics, references is symbolReferences' algorithm from the outside (offer every same-named identifier token back to the analyser and keep the ones that resolve to the same decl, so a shadowed local is not a false hit). What it does not do, deliberately: - Nothing is cached between queries. Each `query` builds a DocumentStore, resolves imports and throws it away, because the arena dies on return and `req.source` is a snapshot of a buffer the user is still typing into. So cold IS warm — there is no index to warm up. It is also fast enough not to need one: 124us for a local goto, 2.8ms into the stdlib, 8ms for references over a 5000-line file. A cross-query cache is a real design (a global, a mutex, an invalidation story), not a line of code, and it is the obvious next step rather than something smuggled in here. - References, rename and select-refs are THIS FILE only. Workspace-wide means loading every project file into the store and running the analyser over each; DocumentStore's own workspace iteration has the same limit (it can only see handles already loaded). Workspace symbols and workspace diagnostics DO walk the tree, because neither needs the analyser — a parse and a tree walk each. - Rename previews, format reports, code actions list. The seam hands back rows, not edits, so there is no channel through which a backend could rewrite the buffer. These answer the question the keypress asks and change nothing. - Without a zig binary, `@import("builtin")`, `@import("<pkg>")` and `@cImport` resolve to nothing — silently, which is ZLS's behaviour, not a bug introduced here. Relative imports and `std` work. - Non-.zig files answer nothing. The core does not gate the keymap by file type, so the gate is here: `gd` in a README must find nothing rather than parse prose as Zig and confidently resolve a word out of it. A whole-file report that ran and found nothing says so ("no diagnostics", "already formatted", "no code actions") rather than returning zero rows, because in this seam zero rows already means "no backend" — `lspResponse` opens nothing for an empty answer, so silence cannot also mean "checked, clean". Location queries keep the opposite rule: unresolvable is no rows. test/snapshots/lsp.snap covers the round trip end to end — gd jumping on a single result, SPC k opening +Hover, SPC s opening the +Search list that n steps, and gd on a keyword answering nothing without opening anything. The whole backend was also fuzzed at 20k queries over real, truncated and byte-smashed sources across every kind; that found two crashes (a decl's name token indexes its own file, not the requesting one, and is not necessarily an identifier at all on a half-typed line) which are fixed. emscripten does not get the backend: the web shell has no threads and no-ops the lsp effect, so it keeps the empty one the base tree shipped. DEPENDENCY: ZLS is FETCHED by the build system (build.zig.zon .url + .hash, pinned to commit 3e0d0820 on the 0.16.x branch) rather than a path dependency on the local genizah checkout, so it lands in zig-pkg/ like every other dependency and the build is reproducible from the .zon alone. Also passes -Dversion-string: ZLS's build.zig names itself by shelling out to `git describe`, and a fetched package is an extracted tarball with no .git, so every build printed a 'Failed to run git describe' warning. We pin the commit, so we already know the answer.
* pardes v2: the rewrite, complete and organized. src/ (core + three shells), ↵Gabriel Schneider2026-08-01
| | | | test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
* sdl ui backendGabriel Schneider2026-08-01
|
* more treesitter grammarsGabriel Schneider2026-08-01
|
* merging the colors intoGabriel Schneider2026-06-30
|
* images? lolGabriel Schneider2026-06-30
|
* ghostty setupGabriel Schneider2026-06-25
|
* day-00: basic mouse and showing cwd relative fsGabriel Schneider2026-06-25