<feed xmlns='http://www.w3.org/2005/Atom'>
<title>pardes.git/src/output_pane.zig, branch main</title>
<subtitle>Pardes</subtitle>
<id>https://git.0x4200.cafe/pardes.git/atom?h=main</id>
<link rel='self' href='https://git.0x4200.cafe/pardes.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/'/>
<updated>2026-09-07T16:59:12Z</updated>
<entry>
<title>Refactor panes and filesystem; replace FUSE with 9P</title>
<updated>2026-09-07T16:59:12Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-06T21:11:36Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=60367d8fe23f6af98ec28e3cf6c2094dfe332df0'/>
<id>urn:sha1:60367d8fe23f6af98ec28e3cf6c2094dfe332df0</id>
<content type='text'>
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples.

Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
</content>
</entry>
<entry>
<title>messages: a fixed log of what the rows said, and a word to read it back</title>
<updated>2026-09-06T20:14:15Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-06T20:14:15Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=1852dd3c1247ee66b4265b0a7ee8b3afd08a8fa5'/>
<id>urn:sha1:1852dd3c1247ee66b4265b0a7ee8b3afd08a8fa5</id>
<content type='text'>
A message row is cleared by the next keystroke, so anything reported while you
were looking at another pane was gone before you could read it — a save that
failed, a watcher's reload, a builtin's complaint. `setMessage` now records
into a fixed ring first: no allocation and no failure path, because it sits
underneath `reportError`, which is reached from sites that are reporting an
allocation failure. `Messages` (`SPC h m`) reads it back oldest-first.

Three things an adversarial pass found, each of which defeated the feature:

PROGRESS IS NOT A MESSAGE. A language server emits `Indexing 47%` several
times a second, and every tick is a distinct string BY CONSTRUCTION, so no
de-duplication can collapse it: at the client's one-per-150ms throttle it
takes about nineteen seconds to push every real message out of the ring. A log
that one indexing run empties is not a log. That path is `setStatus` now —
the row, and nothing else.

THE CLOCK MADE EVERY HOST MESSAGE UNIQUE. `message.stamp` prefixes `HH:MM:SS`,
so `saved /x.zig` at 14:32:07 and at :09 compared unequal and the ring filled
with rows that look identical and each say (x1) — exactly the case the
de-duplication exists for. It compares `message.body` now, the row without its
clock, and the newest wording wins so the row carries the last time it
happened rather than the first. It also keys on the PANE (one pane's failure
must not be recorded as another's) and compares the truncated form, so two
identical messages over 256 bytes stop being two rows.

AND THE CAPACITY BELONGS IN limits.zig. 128 entries is 32.75 KiB that is
allocated whether or not anybody reads it — 8.5% of the ESP32-P4's whole
384 KiB heap, about the size of its effect ring. The board takes sixteen.

The builtins/leader goldens move because the listing gains a row, and
builtins.snap middle-clicks a SCREEN COORDINATE that Tutor moved out of; both
updated selectively and verified against a fresh run.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
</content>
</entry>
<entry>
<title>pipe: a filter that fails says so, and `| head -1` stops failing</title>
<updated>2026-09-03T19:19:23Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-03T19:19:23Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=0d0691b7bad87763d2e0ad1a0ae90f5e3591c663'/>
<id>urn:sha1:0d0691b7bad87763d2e0ad1a0ae90f5e3591c663</id>
<content type='text'>
Two things made the selection pipe feel like it had never worked. It runs —
test/snapshots/pipe.snap drives the real binary through a pty and filters
`alpha beta` to `ALPHA BETA` — but it had no way to tell you when it did not,
and one of its failure conditions was not a failure at all.

IT NOW SAYS WHY. `runOne` read the command's stderr into memory and freed it
two lines later, unread; every caller answered a failed filter with a bare
`return`; `pipeResponse` had eight more silent exits under that. So `| trr`
(a typo), `| grep nomatch` (exit 1), `| jq .` on bad JSON — all did nothing,
said nothing, and left the text alone with no way to find out why. The runner
carries a `Failure` home instead: which selection, what became of the command,
and its own stderr. The core turns that into an `+Errors` buffer — acme's name
for output that came from the program rather than from a word anybody clicked:

    | trr

    exit status 127
    sh: line 1: trr: command not found

An output buffer rather than the message row because the useful half of a
shell failure is the text the shell wrote, and a 256-byte row would keep the
label and throw away the reason. Focus stays with the file: `openRead` moves
`p.active` to what it opens, which is right for a Grep you asked to read and
wrong for a report you did not — you want to fix the command and press `|`
again. A host with no `pull_pipe` at all (the detached daemon, the browser,
the board) now says that too, instead of answering failure into the void.

`| head -1` NOW WORKS. `writer_context.ok` was part of the success condition,
so a command that stopped reading its stdin failed the filter even though it
had done exactly its job: `head` takes the line it wants and closes the pipe,
the write gets EPIPE, and a selection bigger than the 64 KiB pipe buffer was
enough to trigger it. helix joins its input task and ignores the result for
this reason; the exit status is the whole verdict. Also reported rather than
swallowed: the ten-second timeout, the output ceilings, and a file edited
while the filter ran — one keystroke during a slow command used to discard the
result in a way indistinguishable from the filter doing nothing.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
</content>
</entry>
<entry>
<title>lsp: a protocol client for every other language, narrated on the message row</title>
<updated>2026-09-01T14:24:12Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-01T12:23:53Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=ae9325a5cb128d0d952afb8f9feaaca68e5e37a2'/>
<id>urn:sha1:ae9325a5cb128d0d952afb8f9feaaca68e5e37a2</id>
<content type='text'>
The seam grows a second backend: src/lsp/lsp_client.zig speaks JSON-RPC to
child language servers — rust-analyzer, clangd, gopls, tsserver, pyright are
rows in a spec table — while the in-process ZLS analyser keeps .zig. One
reader thread per server owns the socket, routes responses to a mailbox
under the conn mutex (monotonic condvar), answers server-to-client requests,
feeds the diagnostics store, and narrates $/progress and state changes
through a status sink both native shells post to the transient message row:
"rust-analyzer: cargo check 88% 955/1083" lands where a save narrates, with
the same clock. Chatty progress is throttled and deduplicated; settled
states always land, which is also what makes the goldens deterministic.

Nothing wedges and nothing healthy dies: waits are deadline-bounded, a
timeout cancels and returns no rows, three consecutive timeouts restart the
server ONLY while it is idle (an indexing server is narrating its own
excuse), spawn and handshake failures back off 10s to 2min, a crash shortly
after ready counts as a failure, and only a missing binary disables a spec.
PARDES_LSP_{RS,C,GO,TS,PY} override binaries; empty disables; the snapshot
harness pins RS to test/lspmock.zig and empties the rest.

Mutating answers really mutate now: the @put record beside rename @edit
carries per-range text, so = applies the formatter (both backends) and a
same-file WorkspaceEdit rename applies atomically, one undo step, narrated
("renamed 2 range(s)"); a multi-file rename previews as rows instead of
half-applying. Malformed responses fail closed: coordinates validated not
clamped, one bad TextEdit poisons the whole edit set, poison frames kill
the connection instead of buffering forever, decoded control bytes reject a
uri, hierarchy items too deep to reserialize are skipped.

Four kinds helix does not have, on SPC l: c/C incoming/outgoing calls (rows
are call sites), t/T super/subtypes. Pull diagnostics (3.17) preferred when
advertised. Help gains a language-keys footer for the motions no builtin
row could carry; lsp.rel and look.grep now share one path-shortening rule.

zig build lspprobe drives the seam from the CLI (comma-separated kinds share
one server); measured against a 1083-crate workspace warm: gd 26ms, gr 213
rows 165ms, incoming calls 212 sites 197ms, document symbols 670 rows 347ms.
docs/lsp.md tells the whole story; lsp-evaluation.md gets an addendum.
</content>
</entry>
<entry>
<title>acmefs: pardes --fs serves acme's control filesystem over raw Linux FUSE</title>
<updated>2026-08-25T12:42:07Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-25T05:07:23Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=6f48508aa08396bcf9dd4da2cab1d221bcc53f78'/>
<id>urn:sha1:6f48508aa08396bcf9dd4da2cab1d221bcc53f78</id>
<content type='text'>
</content>
</entry>
<entry>
<title>host: the core owns the event loop; every platform becomes a vtable of optional methods</title>
<updated>2026-08-25T12:42:07Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-24T13:56:05Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=c3d0b84b7961ae26d2d654e7120821cc2d83d20d'/>
<id>urn:sha1:c3d0b84b7961ae26d2d654e7120821cc2d83d20d</id>
<content type='text'>
</content>
</entry>
<entry>
<title>big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh</title>
<updated>2026-08-19T02:44:42Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-16T18:49:12Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=1551e409c31992437cb2fa864f576d45c8433801'/>
<id>urn:sha1:1551e409c31992437cb2fa864f576d45c8433801</id>
<content type='text'>
</content>
</entry>
<entry>
<title>look: richer path/range parsing, pdf rendering, corner-drag and stepgrain snapshots</title>
<updated>2026-08-15T14:57:13Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-14T16:40:29Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=f67fec978a9296c651ec06bd2f43686d34ff86ee'/>
<id>urn:sha1:f67fec978a9296c651ec06bd2f43686d34ff86ee</id>
<content type='text'>
</content>
</entry>
<entry>
<title>clipboard, n/N and the tty prompt: three things that were half-wired</title>
<updated>2026-08-12T16:04:32Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-11T21:30:13Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=b424164922842796619cb6894ec46d729a8a6826'/>
<id>urn:sha1:b424164922842796619cb6894ec46d729a8a6826</id>
<content type='text'>
Three changes that all turned out to be the same shape -- a feature that
worked in one direction, or for one pane kind, and quietly did not in the
others.

CLIPBOARD. Every register write emitted set_clipboard, so deleting one
character threw away whatever the desktop was holding; multi-cursor yank took
the join's early return and emitted nothing at all, so the same key reached
the clipboard on one cursor and not on two. Nothing could READ the clipboard:
the SDL shell had no SDL_GetClipboardText anywhere in it, and the tty shell
never asked for OSC 52, so `p` from another application was dead in both.

Now it is helix's split. y/d/c/p/P/R and the acme chords are the DEFAULT
REGISTER and nothing else; the system clipboard is five words on helix's own
letters -- SPC y, SPC Y, SPC p, SPC P, SPC R -- spelled as builtins so they
land in Help and are executable like every other verb. The one exception is
the tag `y` chord, which still mirrors out because a tag is always insert, so
SPC cannot be pressed there, and copying the path out is the whole point of
the chord.

Reading is a new read_clipboard effect answered by an ordinary Event.paste, so
the round trip is honest about being one: SDL and NSPasteboard answer inside
the same drain, the browser answers a promise, and a terminal answers over
OSC 52 or -- far more often -- refuses. A refused read is a paste that does
not happen, and the request dies at the next keystroke rather than landing
minutes late in whatever pane is focused by then.

The tty shell also enables BRACKETED PASTE now and coalesces
paste_start..paste_end into one event. Before this a paste arrived as a flood
of individual key presses: plausible in insert mode, and in normal mode every
pasted character ran as a command.

n/N. They stepped the armed results buffer and immediately Looked each row, so
you could not walk past a hit without opening it. They are a MOTION now:
select the next look-able text, open nothing, and let Enter decide. What they
step is the largest whitespace-delimited run look.resolve can act on
(look.lookableSpan, wrapper punctuation peeled), over a RING of panes -- every
pane that has performed a look, most recent first, then the output buffers
that have not, newest first, and only if both are empty the pane in front of
you. N is the exact inverse of n, computed rather than remembered: both
directions ask the same question about the same spans and compare against the
column the walk parks on, so x presses one way and x back land exactly where
you started, pane boundaries and the ring's seam included.

A ring rather than a list with two ends because a shell's cursor sits at the
prompt, below everything it has printed, so a walk that could not come round
would have nowhere to go on the very first press -- which is the case n/N were
written for.

One motion everywhere, no pane-kind or buffer-kind special case. The only
thing a buffer may change is the GRAIN of what a step selects, and it does it
with one flag rather than a branch: output_pane.Traits.commands (renamed from
`executes`, which named one reader's behaviour rather than the fact) makes a
row select WHOLE, because a ThemeSel line is a word to run and has no path
inside it to pick out. `]d`/`[d` are not n/N -- they are helix's diagnostic
motions, their job is to ARRIVE, and they still reach searchStep.

THE TTY PROMPT. Leaving raw tty blanked the prompt row, and the command you
had typed at that prompt shares the row, so it went too -- a shell out of tty
read as output only. OSC 133 marks the row CELL by cell, so the two are
separable: config.tty_blank = .prompt cuts the prompt's own columns and leaves
the command, left-hugged at column 0 in line with the output under it rather
than in a bay of blanks. .prompt_and_input is the old behaviour, kept.

Because the row is now something you can put a cursor in, enterTty adds the
hidden prompt width back before asking ghostty to walk the shell's own cursor
to it -- the modal column on a cut row is short by exactly that much.

Verified: unit-test 186/186 (nine new), snap 87/87 (new ttyprompt.snap),
hxdiff 481 and hxparity 561 with 0 mismatches, tty and gui both build. And
against the real binaries rather than the harness: in a pty, SPC y emits OSC
52 carrying exactly the selection while plain y emits nothing, SPC p issues
the read and pastes the reply, and a bracketed paste of "dd..." inserts text
instead of deleting two lines. In a real SDL window, SPC y then SPC p round
trips through the system clipboard while the default register holds different
text. Setting tty_blank back to .prompt_and_input reproduces all 86 old
goldens byte for byte.
</content>
</entry>
<entry>
<title>macos: pixel attachments, live theming, and a signed app</title>
<updated>2026-08-11T12:58:59Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-08-10T20:23:18Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/pardes.git/commit/?id=a797a1ab2f648e773f7a1b28d12bf9441b9f13f4'/>
<id>urn:sha1:a797a1ab2f648e773f7a1b28d12bf9441b9f13f4</id>
<content type='text'>
The AppKit shell now draws what the core renders, follows the theme without a
relaunch, and builds into something you can hand to someone.

- Pixel attachments. Surface.images was dropped on the floor here, so a PDF
  pane showed nothing at all: native_images is now set, pardes_image_s carries
  the geometry the core already clipped, and PardesView keeps one CGImage per
  (serial, page, revision) so scrolling costs a draw and not a decode. Image
  panes get real pixels instead of the petscii fallback.

- Themes take hold live. pardes_tick never advanced the chrome animation, so
  every tagline kept the previous theme's colours until the next launch and
  the 16 ms re-pump spun for the rest of the session. pardes_theme_bg retires
  the hand-agreed #121212 and drives the window background and the titlebar
  appearance; a theme with no background of its own now gets a transparent
  window over an NSVisualEffectView.

- The cell snaps to whole DEVICE pixels rather than whole points. Monaco
  advances 8.4014pt at 14, so ceiling to 9 spaced every column 7.1% wider than
  the face was drawn for.

- The dial is one notch per 10 degrees instead of 20, and a release keeps
  turning in proportion to how hard it was thrown -- ramping up from zero at
  the floor, so a slow twist coasts not a little but not at all.

- A file dropped on the grid is a click plus Look, so it opens beside the pane
  it was dropped on. No drop concept was added to the core.

- The titlebar follows the focused pane: proxy icon, filename, and the dirty
  dot. File.saved_revision is the watermark that last one needed.

- Config (SPC f c) prints the resolved startup config path.

- build.zig assembles, signs and packages the bundle itself; build-app.sh is
  gone. -Dmacos-identity= takes a Developer ID, macos-dmg makes the image, and
  the icon is Glenda.
</content>
</entry>
</feed>
