| Commit message (Collapse) | Author | Age |
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every argv refusal in main.zig was `return error.BadArgs` out of `main`, which
std prints as `error: BadArgs` with a return trace under it — the same shape a
real crash has, for the most ordinary thing a person can do. It also said
`BadArgs` and nothing about which argument, at sites that knew exactly:
pardes: no such option: --hepl
pardes: -n takes 1 or 3, not 'abc'
pardes: one file or directory at a time, and 'b' is the second
pardes: --detach and --attach are opposites: one runs the session, the
other joins one
Try 'pardes --help'.
stderr rather than the `+Errors` pane one function down, because argv is read
before a core exists and the person who mistyped a flag is looking at the
prompt they typed it into. `--attach`'s refusal already answered this way; now
all eleven do. `getcwd` failing is no longer reported as an argument problem,
and a `.url` or `@pN` positional says why a LAUNCH cannot act on it rather
than being swept into the same word as a typo.
AND `Look` ON A FIFO NO LONGER FREEZES THE EDITOR. `readFile` opened with a
plain blocking `open`, so a named pipe with no writer waited forever — inside
the keystroke that asked, with no frame, no message row and, in the tty shell,
no Ctrl-C either, because the terminal is in raw mode. It is `O_NONBLOCK` now,
the read loops answer `EAGAIN` rather than waiting, and `lseek` answering
ESPIPE — a pipe, a socket, a terminal — is refused as `NotAFile`, which the
boot pane spells "that is a pipe or a device, not a document". Without that
last part an unwritten FIFO read as EOF and opened a silent empty pane, which
says less than the hang did. The zero-size files worth streaming (procfs and
its kin) seek fine and are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
stays one line
Two defects in the +Errors boot, both found by adversarial re-review.
The pane was opened with `dir = ""` — copied from the board's boot buffer,
which can afford it because that platform has no filesystem — so its path came
out `/+Errors` and `paneDir` answered `/`. An output pane's directory is where
a `Grep` from it walks, where its `Newtty` spawns a shell and what its `Save`
prefills, so the boot screen rooted all three at the filesystem root, and the
one word the pane prints resolved against `/` and could never be clicked. The
launch directory rides in `Options.missing` beside the word now, and the test
asserts the pane's path rather than only its contents.
A typo INSIDE pardes stacked a second full-screen UI. The hand-off block above
resolves the word and sends it to the outer instance; `.none` sent nothing and
fell through, which was harmless while the classification below refused it and
became the one input that stacks the UI that block exists to prevent — with no
shell pane in it, so the only way out is `Del`. Its own comment said as much
and was falsified by the +Errors boot. `.none` is refused in that shell now, in
one line and without a stack trace, and the outer session is not told: `Look`
on a word naming nothing is not something to do to somebody else's session.
Also recorded, not fixed: the commonest permission case never reaches the
`.dir` arm this arm's comment defends. `look.isDir` probes with O_DIRECTORY|
O_RDONLY, so a directory you cannot read resolves as `.file` and dies in
`file_pane.open` with `error.OpenFailed` out of `main` — still a trace at a
human, and a different fault than the one fixed here.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adversarial re-review, confirmed against std's source and then measured.
`captureCurrentStackTrace` is not the safe half of `writeCurrentStackTrace`.
`StackIterator.init` picks the `.di` strategy whenever `SelfInfo` can unwind,
`stratOk` accepts `.di` regardless of `allow_unsafe_unwind`, and `.di` takes
`SelfInfo`'s rwlock EXCLUSIVELY on its first call — the only kind of call a
panic record makes — across `dl_iterate_phdr`, a DWARF CFI machine and an
allocation. A panic in there (a smashed stack is a leading reason to be in a
panic handler at all) leaves the lock held, because the unlock is a `defer` in
a frame that never returns, and `defaultPanic` then waits on it for the life of
the process. A crash becomes a hang, which is worse than what this file was
added to improve on. The frames stay on stderr, where defaultPanic prints them
under the staging that makes them safe; the record keeps what can be gathered
without asking the process any questions.
ONE record per process, never released. With the guard released on the way out,
one panic wrote two records: the real message, then "reached unreachable code"
under it. That second panic is this handler's own `vaxis.recover()` running a
second time — it closes the vaxis tty and never clears the global saying there
is one, so the double close is `recoverableOsBugDetected` and an `unreachable`
in a Debug build. Guarded now in both the panic and the segfault handler; that
half is a fix older than the crash file.
`clock_gettime`'s return is checked, unlike dump.zig's, because a failure here
leaves `ts` undefined and an undefined large-positive `sec` walks
`calculateYearDay`'s u16 year past 65535 and overflow-panics inside the panic
handler. Debug fills it with 0xaa and lands in 1970, which is why it reads as
harmless.
Verified end to end with a temporary probe: one panic, one record.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`pardes nosuchfile` returned error.BadArgs out of nativeMain, 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 something look.resolve cannot make a target of now boots one +Errors pane
filling the window, saying `file or directory not found` and the argument AS
TYPED: acme's own vocabulary for output that came from the program rather than
from a word somebody clicked, and the word rather than a resolved path because
`pardes ~/notes/tdoo.md` wants to see its own typo back.
A chdir that fails on a directory that really is one stays BadArgs. That is a
permission problem rather than a typo, and the two want different answers.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lose it
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; the
SDL and AppKit shells have no terminal at all; a --detach session's goes
wherever its launcher left it. src/crash.zig appends a record to
<config dir>/crashes first: one line naming the build (version, commit, UTC,
os-arch, pid) and under it the panic message and the frames behind it.
RETURN ADDRESSES and not the symbolised trace, which is measured rather than
chosen. `std.debug.writeCurrentStackTrace` called from a panic handler BEFORE
defaultPanic wedges the process at 0% CPU: symbolising reads DWARF, that read
can itself panic, and the staging which turns a nested panic into "aborting due
to recursive panic" is defaultPanic's own and private. Reproduced in a
standalone build with this program's std_options_debug_io and inside a test
binary. `captureCurrentStackTrace` only walks frames, so the addresses go in
the file and `addr2line -e` finishes the job; stderr still gets the symbolised
trace from defaultPanic, unchanged.
The AppKit shell gets a panic handler of its own here too: the macOS build
roots at macos.zig, so main.zig's had never run there — in the shell with the
least useful stderr of the four. The config directory is COPIED rather than
borrowed, because that host's lives in an arena its own errdefer frees. One
record at a time, so two panicking threads cannot interleave into one buffer.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Step 5 of the 9P chain (docs/9p.typ 12.5, docs/registry.typ 9P-22, 9P-11, BOARD-1).
THE CLIENT. `Client` in src/9p.zig is the mirror of `Server` and the same shape:
sans-io, no allocator, no threads, no descriptor, caller-owned buffers, and it
builds freestanding. 152 bytes of struct against the server's 9,488, because a
client owns neither a fid table nor a park table -- the far end does.
The API is submit / push+output+wrote / take. Completion is a PULL: a callback
would fire inside push, inside the transport's read, inside the host's poll
dispatch, which is exactly where fs9_service says filesystem work must not
happen. `take()` returns the next completed operation or null, which is
`Server.next()`'s loop-until-null contract read from the other side. Tags are a
fixed 16-entry table indexed BY the tag, so an out-of-order reply -- which 9P
allows and both reference clients rely on -- costs one bounds check. The reply's
TYPE is checked against the request's op, because a tag is only as good as the
table behind it. A `Done` borrows the input buffer and is valid until the next
call; `take()` releases the previous frame on entry, so the rule is mechanical
rather than remembered, and read data and error strings are zero-copy.
And one real caller, so this is not a library with no user: the `9p` word takes
a dial and a path, walks another instance's tree, and opens the bytes in a pane
like any other `Look`.
THE BOARD. A SECOND image, not a second role: the console runtime keeps UART0
bidirectionally and is behaviourally untouched. On the new one the UART carries
9P AND NOTHING ELSE -- no ANSI, no vaxis, no allocator, no heap module. The loop
is uart.read -> push / retry+next -> handle -> reply / output -> writeSome ->
wrote. `writeSome` is new and additive: `write`'s bounded spin DROPS bytes on a
stalled transmitter, which on a protocol stream truncates a reply mid-message
and desynchronises for good, where a short count cannot. BOARD-1's one divider
write raises the line to 921600.
88,000 B text, 49,424 B bss, an 88,080-byte image -- 5.7% of the 1,536,000 B
partition, against the console image's 809,536 B.
THE COMPTIME BRIDGE, which is the part worth reading. `board9p.caps` is the ONLY
place the GPIO tree is described; node ids, parents, names, permissions,
handlers, buffer size and the per-pin directories are all derived from it, and
`fan.dirs` makes `gpio/<n>/value` one table entry serving eleven pins. Modes are
derived from which handlers a file has rather than declared. A second capability
is a table entry, not new tree code.
JP1 became a real table in the new leaf `src/board_pins.zig`, with the ASCII
drawing RENDERED from it at comptime and the pin list COLLECTED from it -- the
9P image links no core and so cannot import board_memory.zig, and copying the
table was not acceptable. A golden test pins the drawing byte for byte, the
console's own shape test still passes, and the identical bytes are present in
all three artifacts.
PROVED. Two daemons: B read A's `/1/body` through the `9p` word into a pane,
byte-identical to plan9port's `9p read` of the same path. Both board images
build. No hardware was attached, so nothing about the board is claimed beyond
what builds and what the host tests cover.
zig build unit-test 585/585. fs-bench unchanged and still zero allocations on
every read row.
---
REVIEW FIXES FOLDED IN. Steps 3, 4 and 5 were verified on the happy path and
then adversarially reviewed by three agents; eight defects, six fixed here, five
of them reproduced with measurements before and after. Full writeup in
docs/registry.typ `9P-27`. In brief:
* a remote crash of the WHOLE daemon: one `size[4]` of zero plus one byte hit
`unreachable` in `fs9_service.fill`. Also 99.7% of a core when the stuck
buffer made `room == 0` return without reading. Now `srv.dead` is a hangup,
checked before the room guard.
* the editor froze 177 s on a dial: `connect(2)` ran on a still-BLOCKING
socket before the deadline existed, and a full accept backlog waits forever.
Now non-blocking with the wait spent against the budget. After: 2.03 s.
* a 64 KiB pty read is exactly `queue_cap` and wiped every unread byte AND
dropped itself. `notePtyOutput` splits at half the cap. Deterministic.
* four silent sockets denied `--fs9` forever; connections now expire on the
same five-second rule the frontend transport already had.
* EMFILE spun a core; the listener pauses and leaves the poll set, as the
frontend listener does.
* `max_fids = 32` made `find` over `9pfuse` fail with 57 consecutive
`Rerror`s -- refuting this step's own acceptance clause. 256 for a host,
`board_fids` 32 for the microcontroller.
Found clean and worth recording: `sig` reaches the foreground process group; the
two-namespace pty lookup is right over both transports; `PaneFile`'s u4 wall is
guarded; reader counts release on every abrupt-death path; `fs_origin` routing
and the reply arithmetic hold under probing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Step 4 of the 9P chain (docs/9p.typ 12.4, docs/registry.typ 9P-15/16/17/4/5).
src/9p.zig is a base 9P2000 codec and a SANS-IO server: it never touches a
descriptor, takes no allocator, starts no thread, and builds for
wasm32-freestanding and riscv32-freestanding. That is what lets the same code
serve a unix socket here and a UART on the board later.
Server(comptime fs: type) duck-typed on fs.Req/fs.Reply/fs.Reply.Attr, so it
never imports acmefs and acmefs never learns 9P
init{ in, out, root } the caller owns the buffers; msize is derived
retry/next/reply the three fs_service.Transport ops, by name
push/output/wrote/hangup bytes in, bytes out, partial writes supported
next() is a PUMP, not one-message-one-request: a 3-element Twalk is three
lookups, Topen|OTRUNC is a setattr then an open, Tversion is none at all.
Decisions that were open and are now taken, each recorded in the file:
* qid.version is ALWAYS 0, which makes Linux set P9L_DIRECT and skip its
cache -- the 9P equivalent of the FOPEN_DIRECT_IO fuse.zig relies on.
* Every Rread is clamped to the client's count. An over-long one is a hard
-EIO in Linux, not a truncation.
* Rerror carries Linux's exact strerror text (registry 9P-4 option A), so a
mount recovers the errno instead of ESERVERFAULT. Asserted as literals,
because a typo there is 'Unknown error 526' on every mount.
* `.` and `..` are resolved BY THE SERVER. Under FUSE the kernel does it
and acmefs says so; 9P has no kernel, and forwarding `..` as a lookup
would break every client that normalises a path.
* Topen checks the perm bits itself. Under FUSE the kernel enforced them;
over 9P nobody is above the server, and `errors` would have been readable.
* Tcreate and Tremove are Rerror: `new/` creates a pane on WALK, so the
capability exists and is not spelled Tcreate.
THE INTEGRATION BUG, which was not in the protocol: the daemon's push_fs_reply
sent every reply to the FUSE mount, whose park table has no 9P tag, so it
dropped it -- Tversion worked (no core involved) and Tattach hung forever. That
is exactly the 'no routing origin for the 9P descriptor' cell in the layering
table of docs/9p.typ. Session.fs_origin now carries the transport that asked.
Proved with plan9port against a live daemon serving BOTH transports at once:
9p ls / and /1, read index/ctl/tag, write /1/body, stat, a walk through
/1/../index, pane creation through `new/body`, and the two refusals arriving as
strings -- 'permission denied' and 'No such file or directory' -- confirmed on
the raw wire as Rerror text rather than numbers. A write over 9P reads back
through FUSE and a write through FUSE reads back over 9P.
msize 8192, 34,072 bytes per connection (Server 9,488 + in 8,192 + out 16,384,
out being two msize so that every reply is infallible), four connections.
zig build unit-test: 468 tests before, 503 after.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
with no thread
Step 1 of the 9P chain (docs/9p.typ 12.1, docs/registry.typ 9P-14).
A detached session was the one configuration no script could drive. The core,
the panes and the undo history outlive every frontend that attaches -- and the
filesystem that would let a program read or change any of it was never mounted,
because `push_fs_reply` was one of the host methods this process left null.
Nothing prevented it; the call was simply not there.
It costs less here than in the desktop shells. They start a thread that blocks
on poll() and pokes a loop it does not otherwise share (`fs_service.wake`);
this process already runs ONE poll over its listener, its frontends, its pane
shells and inotify, so /dev/fuse is one more descriptor in the same syscall and
there is no thread at all. `Source.fuse`'s arm does nothing on purpose: being
in the set is the whole point, because the wake must end the sleep so that
`pollFrame` -- which runs after `pull_wait_input` returns, where re-entering
the core is legal -- reaches the drain.
`main.zig` refused `--detach --fs` outright, with a comment saying that
serving it would mean mounting FUSE in the detached core and that this was a
feature rather than a fix. It was right, and this is the feature. `--attach`
is still refused: a frontend has no core to serve.
Verified against the project's own clients: examples/acmefs/pardesctl panes,
new, send, body and del all drive a daemon, and the pane shells it forks now
inherit PARDES_FS/PARDES_PANE like every other host's.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Esc stops recentring
## 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`-Dplatform=p4 -Dtarget=riscv32-freestanding` emits a single freestanding OBJECT
exporting a seven-function C ABI, not an executable. The board's toolchain
(../05-zig-p4) owns `_start`, the linker script and the UART driver and links this
in. The seam is bytes rather than types, so neither side can accidentally depend
on the other's internals, and a signature that drifts fails at link time.
The serial line is the whole of the I/O. `src/p4.zig` drives vaxis unchanged over
it: the renderer is a byte writer and `queryTerminalSend` is a byte writer, so the
terminal emulator on the host answers the capability handshake and the firmware
sees a real terminal. Measured going out over the wire on attach: alt screen,
in-band resize, cursor report, kitty keyboard, kitty graphics, DA1.
THREE WORDS EXIST ONLY HERE. `src/board_memory.zig` implements `Peek`, `Poke` and
`Hexdump`, gated on `builtin.os.tag == .freestanding and !isWasm()` - derived from
the TARGET, because they are a property of running with no OS under you rather
than a product option, and because wasm is freestanding too and is exactly what
must be excluded: in a browser an address is an offset into the linear memory this
editor's own heap lives in. Every access goes through `*allowzero volatile`: a
peripheral register is not memory, and address 0 is an ordinary unmapped address
on this bus. One 4 KiB cap per command, set by the console rather than the memory -
an unbounded dump would wedge the only console the board has for eleven hours.
Measured on ESP32-P4 rev v1.3 silicon, driven from a host terminal:
Peek 0x501101a4 0x0e63ce71, then 0xaeaa6919 on a second read - the
RNG register, so the volatile loads are not folded
Poke 0x5011002c 0xdeadbeef LP_STORE0; a later Peek returned 0xdeadbeef
Hexdump 0x5011002c 32 16 bytes a row, hex columns and an ASCII gutter
Peek 0x50110001 `peek: MisalignedAddress` on the message row
That last line is the one that matters. A misaligned 32-bit access traps, and a
trap in firmware is a watchdog reset that takes the session with it, so the check
that turns it into a message is the reason the file is hand-written rather than a
generic reader.
BARE METAL BOOTS AN EMPTY OUTPUT BUFFER. Every other boot layout in `init` makes a
shell, and on this platform that is not a preference but an impossibility: nothing
to fork, no pty to give a terminal pane. Booting one anyway produced precisely what
that describes - a pane whose tag ends in `Filter`, no gutter, no buffer, and every
keystroke vanishing into the Fallback's silent pty. An output buffer is also what
the platform's own words want, since Peek, Poke and Hexdump each fill one.
Sized for the board rather than for a desktop:
* `allocators.zig` gains a p4 tier that is ALL fallback - every capacity is zero,
so each arena spills immediately to the 384 KiB heap the firmware hands over,
and no megabyte-shaped static reservation lands in `.bss`.
* `source_manifest.zig`'s allowlist is EMPTY on p4. The table is ~0.95 MiB of
rodata against a 1.5 MiB flash partition; the firmware's filesystem is the
serial host's, through the Host vtable.
* The grid is clamped and the clamp is measured, not guessed: every cell is paid
for four times (vaxis Screen + InternalScreen, pardes Surface + previous_cells),
so 40x12 fits and 80x24 exhausts the heap during `Pardes.init`.
* `Vaxis.resize` deinits both screens before allocating replacements, so a failed
resize leaves vaxis rendering nothing. The p4 shell keeps the previous geometry
on failure instead of leaving a half-applied one.
Also here: `output_pane_integration_test.zig` had an exhaustive switch over
`Platform` that adding `.p4` left unhandled, which broke `zig build unit-test`
outright - the native test binary is the one consumer no platform build compiles.
346 tests pass again.
|
| | |
|
| |
|
|
| |
docs
|
| |
|
|
| |
+ snapshot refresh
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adds -Dplatform=macos, a fourth backend beside tty, gui and web. Zig keeps the
core, the ptys, every effect and the worker threads; Swift owns NSApplication,
the window, input translation, and drawing the cell grid with CoreText. They
meet at a hand-written C ABI in src/macos/pardes.h, built as a static library
the app links.
The ABI is src/web.zig's boundary with the wasm removed, because both hosts are
the same animal: someone else owns the clock, feeds events in through flat
functions, and reads one packed cell buffer out. The browser proved the shape.
The one divergence is that the browser has no processes and forwards every
effect to JavaScript, whereas forkpty is right here, so src/macos.zig performs
them — spawn, write, resize_pty, save_file, new_file, write_dump, open_link,
set_clipboard. lsp, pipe and watch are answered with nothing and marked; the
core already tolerates that, since the browser answers none of them either.
This deliberately inverts ghostty's split, which was studied first and is
written up in docs/ghostty-macos-notes.md. Ghostty hands Zig a bare NSView*,
installs its own CALayer and owns the frame clock; Swift never renders. Pardes
does the opposite because its frame is already a cell grid and CoreText draws
one natively — the alternative is a second hand-rolled glyph atlas, which is
what most of gui.zig's 4,300 lines already are. It would also have been written
blind: the Swift half cannot be compiled here.
What makes the scaffold verifiable rather than dead code is that the Zig half is
ordinary POSIX and builds and tests on Linux. Borrowing ghostty's best trick,
build.zig translate-C's the header into the test build and src/macos.zig asserts
every constant, struct layout, and exported function's arity and widths against
it. That guard earned its place immediately: pardes_scroll grew a cell
coordinate after the Swift view had been written against the older form.
Skipped, and named as the upgrade path in docs/macos.md: the Xcode project,
xcframework, lipo and codesigning ghostty needs. All four exist for
distribution; a dev build is a swiftc invocation and a directory with a plist.
The Swift app is a scaffold and says so — every uncertain API spelling carries
an UNVERIFIED marker, and no part of it has been compiled.
tty is unaffected: 75/75 snapshot scripts and both unit suites pass.
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
file:LINE:COL-ENDLINE:ENDCOL, with the two short forms people actually type
reading naturally: file:412:9-21 on one line, file:412-418 whole ones. Ends are
inclusive. A path feature, not a search feature — a ranged path typed in a tag
or middle-clicked out of a shell's output selects just the same; search is only
its first consumer.
The dash is the fussy part. `-` was already a file char, so a ranged word
survives click expansion whole, but a range needs a number on BOTH sides or
my-file:10, build-2 and 2026-07-30 would stop being paths. Table-driven test in
look.zig for exactly that.
Selecting goes through the cellRange/setPaneRange pair the multi-cursor work
left, and hxOff clamps both ends, so a stale range selects what still exists
rather than crashing or reaching past EOF — pinned with an 8:6-400:9 range in a
nine-line file.
Producers: / search, Grep, and five LSP sites through a new spanRow — goto,
references, rename tokens and both symbol lists were throwing away real
protocol ranges at path:line:col. Left alone deliberately: Find rows are bare
paths with nothing to span, a jump is a spot not a span, and the diagnostic and
format paths only ever have a point, where half a range would be worse than
none.
One knock-on worth knowing: n now leaves an EXPLICIT selection, so a topbar
execute chords it. grep.snap's no-match step was silently becoming
`Grep TARGET`; it runs from the leader path now, which never chords, and the
dedicated chord steps stayed where they were.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
their own files
The core now lies FLAT at src/ and every subdirectory is one backend, so a
file being in no directory at all is what says it is core. Pane-kind bodies
leave pardes.zig for term_pane.zig / file_pane.zig / output_pane.zig, leaving
it the layout, the event/effect machine and the generic render loop.
Builtins are one struct each in builtins.zig, and the enum is folded out of
the file's own declaration list at comptime — a zig file IS a struct, so the
list of builtins and the builtins themselves are the same text. Adding one is
writing a struct. Key paths deliberately stay one table for the config pass.
Pure refactor: no golden moved.
|
| |
|
|
| |
painting over the UI
|
| | |
|
| |
|
|
| |
(file:LINE works) opens it focused in the left column with a greeting terminal on the launch dir at right, a DIRECTORY arg chdirs so shells spawn there. New argv.snap; boot/restore.snap pin -n 1 to keep the classic boot.
|
| |
|
|
| |
(rust/cpp/python/...; e.g. opening agave cpi.rs or any tracy .cpp), Debug AND ReleaseSafe. Root cause: clang -fsanitize=function (in zig's default C UBSan set) traps at the runtime's indirect call of the scanner because grammars declare external_scanner_create() with EMPTY PARENS — a K&R non-prototype whose type hash differs from the void*(*)(void) pointer type. The ud1 trap lands on a bogus inlined line (stack.c:746), which cost the diagnosis a detour through rr (its gdbserver dies replaying past the task exit — core dump + coredumpctl worked; ud1 0x6(%eax) = SanitizerHandler kind 6 = function_type_mismatch; scanner-less c/zig grammars never crashed). Fix per review direction: -fno-sanitize=function on the grammar TUs in build.zig — uninstrumented callees make the runtime's call-site checks skip; the rest of UBSan stays live. Second half: fatal signals (SIGILL/SEGV/BUS/FPE) never run defers and bypassed the panic hook, leaving the terminal raw after a crash — root.debug.handleSegfault override now runs vaxis.recover() before std.debug.defaultHandleSegfault, verified in a raw pty (kill -ILL $PPID: rmcup + mouse resets precede the trace). Verified: rust/cpp/python opens work with real highlighting (snapstyle: keywords/strings/comments colored), agave cpi.js 2.7k-line open fine, suite 30/30, ReleaseSafe build opens rust identically.
|
| |
|
|
| |
gives the classic two-column boot), -n <count> CLI flag. The 26 legacy snap scripts pin -n 3 on their start line so their coordinates and goldens stay valid; boot.snap tests the new default (golden regenerated: one full-width pane). 28/28 green.
|
| |
|
|
| |
vaxis.Tty.init makes the tty raw but run() never called tty.deinit (termios stayed raw -> no echo, staircased output in the spawner shell); the pty reader tasks were never joined (Kill-quit hung the exit in the runtime thread-join with readers blocked in read(2), and finished readers dumped DebugAllocator leak traces onto the tty). Fix: defer tty.deinit() (runs last, after vx.deinit flushes its resets); cancel each pty reader at teardown (cancel interrupts the blocked read) then close masters and drain the loop queue (queued pty_read/paste bytes are gpa-owned); await the reader on pty_eof before closing; readPty posts its eof via tryPostEvent so a post-cancel full queue can't re-block; root panic handler wraps vaxis.recover() so a panic restores cooked mode/main screen/mouse before the trace prints (vaxis.Panic itself is stale: references std.debug.FormattedPanic which 0.16 dropped). Verified with a scripted pty harness (bash spawner, DSR replies): both quit paths (exit-EOF of last pane, middle-click Kill) end with rmcup+cursor+sgr+mouse-off and identical stty -a before/after, no leak spew, prompt back in 0.5s; snap suite 18/18.
|
|
|
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).
|