From 29ac9be75fdcafbd7d05c15aa9eb8490d74caa98 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 26 Aug 2026 18:58:37 -0300 Subject: An edited row keeps its colours, four copies of forkShell become one, and Esc stops recentring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## A terminal row's ANSI colours survive being edited The loudest colour bug this editor had: one keystroke anywhere in a coloured shell row turned EVERY column of it grey. `EditAnchors` anchored a buffer line only when it was BYTE-IDENTICAL to the shell row it stood over, so a single differing byte dropped the whole row's colour projection. Worst shape is invisible: append past the pane's right edge, where the text is clipped, and the row looks the same and only its colour goes. Anchoring is byte-level now. An edit leaves the row's own bytes at both ends, and being the same bytes they keep the same colours; only what was typed has no cell under it, so only that takes none. Live, on real `fastfetch`: a 32-column blue run split into 6 + 26 around one typed character. Three defects underneath it, all found by machinery rather than by reading: * A JOIN removes a buffer line while the buffer's covered span grows, so `lines == covered` and both aligned guesses — Nth line over the Nth covered row, and the same counted from the bottom — resolved to the SAME wrong row. Every untouched row below a join went plain. Anchoring is now a streaming monotone matching: one shell-row cursor that only ever moves forward, advanced once per buffer line, linear in the buffer where the version before it was quadratic. * An EMPTY line is not evidence. Splitting a row makes one, it equals every blank row in the span, and left free to look ahead it claimed the blank row below the last output and took every coloured row in between out of reach of the lines that owned them. * Reflow under a scrolled viewport. `PageList.getTopLeft(.viewport)` returns the viewport pin verbatim, x and all, while `PageList.pin` forces x to 0 — so after a reflow remapped a tracked pin into the middle of a row, the text pass dumped row 0 from that column while the colour pass paired the fragment with the row's FIRST cells. Row 0 wore its left half's colours until the pane snapped back to live output. `bodyText` dumps from column zero now, which is also what ghostty's own renderer draws. Also here: DECSCNM (reverse video) was silently dropped whenever `tty_filter` was off, because the raw path resolved a `.none` colour by role and never consulted the mode. The test that found the first two is the one worth keeping: random editing against an ABSOLUTE oracle — every row's own text names the colour it must have — because the differential oracle it replaced was blind by construction. It skipped the edited row, which is the row the user is complaining about. ## Esc returns to a pane without moving its view Esc in body normal mode runs `Last`, "the pane you were in before this one", and that went through `focusPaneLine`, which recentred a file on the target line unconditionally. So returning to a buffer repainted the whole screen to show a line that was already on it. `focusPaneLine` takes a landing now: `.center` for the three callers going somewhere you have not been (a look target, a path a pane already holds, `@pN:LINE:COL`), `.keep` for Esc. `.keep` leaves the view alone and lets `ensureCursorVisible` — which already existed and already scrolls by the minimum into the `scroll_off` band — be the only thing that may move anything. Not `line = 0`, which `focusPaneLine` already understands as "focus and touch nothing": a background pane's view can move while you are away, because the wheel scrolls the pane under the POINTER and a resize reveals no cursor, so the recorded cursor plus a minimal nudge is what actually gets you back. Ctrl-o and Ctrl-i keep centring, and the asymmetry is structural rather than arbitrary: `Last` only ever CROSSES panes, so the pane it lands on already holds the view you left it with, while `jumpBy` can land in the SAME pane, where a long in-file jump would arrive on the very top or bottom row with `scroll_off` lines of context on one side. Helix splits the same pair the same way — its jumplist centres, its buffer switch does not. One deliberate consequence: under `.keep` a PDF's page is not restored AT ALL, because a page reveal IS that pane's view and a reveal of the page you are already on still snaps `document_scroll_y` to that page's start, discarding where you had read to. When something moved the pane while you were away — the wheel again — Esc leaves it where the wheel left it, and Ctrl-o is how you reach the recorded page. ## host_io.zig: the machine-local half of a host, once `host.zig` is the seam. The part of the answer that is identical on every host with an operating system under it — fork a pane's shell, put bytes on a disk — was written FOUR times: in tty.zig, gui.zig, macos.zig and detached/server.zig. What those copies had in common says what they were for: all four were missing FD_CLOEXEC on the pty master, so in every shell pardes has shipped, a program in one pane could read another pane's terminal. One copy now, and the wire got smaller for it: `ServerMsg.spawn` is gone. A frontend never asked the server to fork anything — the server has an operating system under it and forks through `host_io` like every other host — and `decodeClient` lost the scratch buffer that message needed. --- README.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 README.md (limited to 'README.md') diff --git a/README.md b/README.md new file mode 100644 index 00000000..daeac183 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# pardes + +A text environment in the acme tradition: columns of panes, each pane a tag line +plus a body, where the body is a live terminal, a file, an image, or a PDF. The +mouse carries meaning — left selects, middle executes, right looks — and +everything on screen is text that is equally alive, whether a shell printed it or +you typed it. + +One program, five thin shells. The core is a library in the way ghostty-vt is a +library: you feed it events, it returns a surface and a list of effects, and it +performs no IO itself. Everything a shell does is translate native input into +`pardes.Event`, render `pardes.Surface`, and perform `pardes.Effect`. + +## Requirements + +Zig **0.16.0** (`build.zig.zon` pins `minimum_zig_version`). Dependencies are +fetched and pinned by the manifest; no system package is required for the +terminal build. The SDL shell builds SDL3 and FreeType from source. Native PDF +support builds MuPDF and is on by default (`-Dmupdf=false` to drop it). + +## Build + +``` +zig build +``` + +That is the whole of it, and it is an *install*: it builds both native shells and +puts them in `~/.local/bin`. + +``` +~/.local/bin/pardes the terminal shell (libvaxis) +~/.local/bin/pardes-gui the SDL3 window +``` + +Override with `--prefix `. Six development binaries install under +`/dev` so they never land on a `PATH` by accident: `perf`, +`fs-bench`, `lspbench`, `pdf-scroll-bench`, `hxdiff` and the isolated build. +The other steps below build what they need and install nothing. + +``` +pardes --version e.g. pardes 0.0.1 (e61bbb2e86bd) +pardes --help every flag +``` + +The version comes from `build.zig.zon`'s `.version`; the commit is read from +`git` at configure time and is simply absent when there is no repository to ask. + +## The five platforms + +| build | what it is | +|---|---| +| `zig build` | the terminal shell and the SDL window, together | +| `zig build -Dplatform=tty` | the terminal shell alone | +| `zig build -Dplatform=gui` | the SDL3 window alone | +| `zig build web` | a freestanding wasm core plus vanilla JavaScript, rendered as HTML/CSS | +| `zig build -Dplatform=macos` | an AppKit and CoreText app over a static `libpardes.a` | +| `zig build -Dplatform=esp32p4 -Desp32p4-firmware` | firmware for an ESP32-P4: a freestanding riscv32 object driving libvaxis down a UART, in 384 KiB of heap | + +The board build needs an ESP-IDF checkout for its register headers, and adds +`esp32p4-flash`, `esp32p4-attach`, `esp32p4-run`, `esp32p4-reset`, +`esp32p4-image-size`, `esp32p4-image-check` and `esp32p4-test`. + +## Detached sessions + +A shell need not be in the same process as the core. + +``` +pardes --detach=work a core with no terminal of its own +pardes --attach=work become a frontend of it +pardes-gui --attach=work ...the SDL window can attach too +``` + +Several frontends may be attached at once and all see the same screen. The +detached core performs every effect that needs a disk or a process table, so its +pane shells outlive every frontend; a frontend keeps only what needs the human's +own display. From inside the editor, `Attach` and `Detach` do the same thing as +words. See `docs/detached.md`. + +## Tests + +``` +zig build unit-test module and shell unit tests +zig build snap scripted input traces against frozen golden grids +zig build hxdiff differential suite against helix's own behaviour +zig build hxparity file-pane vs pty-pane editing parity +zig build mupdf-check compile, link, render and search docs/design.pdf +zig build web-snap browser touch/LOOK snapshots +zig build web-e2e Chrome-driven DOM end-to-end suite +``` + +`snap` and `hxdiff` take `-- --update` and explicit case files respectively. The +benchmark steps — `perf`, `pdf-bench`, `pdf-scroll-bench`, `pdf-sections-bench`, +`lspbench`, `fs-bench` — all accept `-- --json`. + +## Documentation + +`docs/design.pdf` (from `docs/design.typ`) is the architecture document and the +place to start. It is also a test fixture: `mupdf-check` renders and searches it. + +| file | subject | +|---|---| +| `docs/design.typ` | architecture: the seams, the data model, the build graph | +| `docs/detached.md` | one core, many frontends, over a unix socket | +| `docs/config.md` | build options and runtime configuration | +| `docs/acme-fs.md` | the acme control filesystem (`--fs`) | +| `docs/lsp.md` | the in-process ZLS backend | +| `docs/lsp-evaluation.md` | why that backend, measured against the alternatives | +| `docs/helix-keys.md` | the helix-compatible key model and its differential suite | +| `docs/macos.md` | the native macOS shell, its bundle and its signing | +| `docs/web.md` | the browser shell | +| `docs/ghostty-macos-notes.md` | notes on the ghostty dependency | +| `docs/ideas.typ` | scratch notes; nothing compiles it, and it says so | + +`next-steps.txt` is a wishlist with a status header saying which items have +shipped; `transactions.txt` records one open structural gap against helix, and +says which waiver proves it is still open. + +## Layout + +``` +src/ the core (pardes.zig) and one module per platform +src/detached/ the wire, the detached core, the frontend client +src/lsp/ the in-process language backend +test/ harnesses, snapshot goldens, helix cases +build/ build-time helpers (the snapshot suite) +docs/ see above +``` -- cgit v1.3