diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-06 18:11:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-07 13:59:12 -0300 |
| commit | 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch) | |
| tree | 310fc734173cf771881f4691c71909135fadde97 /src/detached/wire.zig | |
| parent | fa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff) | |
| download | pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip | |
Refactor panes and filesystem; replace FUSE with 9P
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.
Diffstat (limited to 'src/detached/wire.zig')
| -rw-r--r-- | src/detached/wire.zig | 243 |
1 files changed, 10 insertions, 233 deletions
diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 8281fc63..ab19bcfb 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -1,227 +1,28 @@ -//! THE DETACHED-SESSION WIRE FORMAT: one `Event` and one `Host.VTable` call per -//! message, byte for byte, with nothing native about the bytes. -//! -//! WHO OWNS THE CORE. The `Pardes` instance lives in the DETACHED process -//! (server.zig). A frontend (client.zig) owns a terminal and a socket and -//! nothing else: it sends the input it collects and draws the frames it is -//! sent. One core per session, N frontends attached to it, all looking at the -//! same screen — `screen -x`, not N sessions. -//! -//! WHY A CODEC AT ALL, when nested.zig's socket carries a builtin command line -//! and has nothing to version: a command line cannot carry a frame, and frames -//! and input are this transport's entire content. -//! -//! ARCHITECTURE-NEUTRAL, and not as decoration: the frontend on the other end -//! may be riscv32-freestanding (the ESP32-P4 board) while the core is x86_64 -//! linux. So: -//! * every integer is an explicit width, little-endian. No `usize` reaches -//! the wire — a pointer-sized field is 4 bytes on the board and 8 here, and -//! every field after it would then be read at the wrong offset. -//! * no native struct is ever blitted. `@bitCast`/`std.mem.asBytes` of a Zig -//! struct puts this compiler's field order and padding on a socket; every -//! field below is written and read by hand. -//! * every union and every enum gets a tag chosen HERE (`ClientTag`, -//! `ServerTag`, `ColorTag`, ...) and never `@intFromEnum` of a core type, -//! so reordering `Event` or `CellStyle.ul` cannot silently redefine the -//! protocol. The mapping switches are exhaustive: adding a variant to the -//! core is a compile error in this file, which is the point of them. -//! * every variable-length payload carries an explicit length prefix, and -//! `max_payload` bounds the lot. This is a parser on a socket: a malformed -//! frame must be REFUSED, never indexed past. -//! * a bool is one byte, 0 or 1. Any other value is a decode error rather -//! than "nonzero is true": a byte this protocol cannot mean is evidence -//! the stream is not the stream it claims to be. -//! * floats travel as their IEEE-754 binary32 bit pattern inside an explicit -//! u32. Both ends agree about binary32; neither agrees about struct layout. -//! -//! BUILD-NEUTRAL for the same reason. `Event.resize.cell_pixels` exists only -//! when native PDF placement is compiled in (pardes.zig `CellPixels`), and a -//! frontend must not have to have been built with the core's options — so it is -//! ALWAYS on the wire and dropped on arrival by a build with nowhere to put it. -//! -//! WHAT IS NOT HERE. The seam has twenty-two methods; this carries FIVE of -//! them — `push_present` as `frame`, `push_set_clipboard`, -//! `pull_read_clipboard`, `push_open_link` and `push_detach` — and the -//! seventeen it does not are named here with their reasons. The five are -//! spelled out because this arithmetic has now gone stale twice in one day, -//! once when the machine-local eight moved into the daemon and once when -//! `detach` arrived, and a count nobody can check against a list is a comment -//! that rots quietly. -//! * The nine machine-local ones — `push_spawn`, `push_pty_write`, -//! `push_pty_resize`, `push_pty_signal`, `push_write_file`, -//! `push_write_dump`, `push_watch_file`, `push_watch_theme`, -//! `push_dump_themes` — are -//! performed by the detached core ITSELF, through `host_io.zig`. A unix -//! socket means it is on the same machine, so there is no question of -//! whose disk or whose process table is meant, and a pane's shell has to -//! outlive the frontend that asked for it or a detached session is a -//! promise it cannot keep. The `ServerTag` doc below carries the whole of -//! that argument; this line exists so the count at the top of the file -//! agrees with it. -//! * `pull_wait_input` IS the server's poll loop, not a message. -//! * `push_poll_frame` and `push_post_present` carry no information. They are -//! per-frame bookkeeping ticks, and `frame` already arrives exactly once -//! per pump at the same place in the order — a frontend does its per-frame -//! work when a frame lands. Two more messages per frame per client would -//! say nothing the frame does not already say. -//! * `pull_tty_taken` and `pull_gpio_toggle` are answers the CALLER waits -//! for, and `pull_lsp`/`pull_pipe` are work dispatched off the loop. A -//! round trip inside `update` is the one thing this transport must never -//! do: the core would block on a socket, and `pull_wait_input`'s own -//! comment is that it is the only place this process may sleep. The -//! process that owns the core answers all four. -//! * `push_fs_reply` cannot be a broadcast. host.zig's rule is that the -//! transport which asked is the one holding the request; with N frontends, -//! N-1 would receive the answer to a request they never made. So the acme -//! mount stays in the detached process, where the `Event.fs_req` that -//! starts it is raised, and neither half of that pair is on the wire — -//! which is also why `Event.fs_req` has no `ClientTag`. +//! Detached input and frames use fixed-width little-endian fields and explicit tags. +//! Native struct layout never reaches the wire. const std = @import("std"); const pardes = @import("../pardes.zig"); -/// Bumped whenever any layout below changes. Checked on connect and refused -/// loudly (see `Refusal.version`): two builds of pardes are routinely on one -/// machine — `zig build` replaces the binary under a running session — and a -/// frontend decoding another version's frame layout would paint garbage and -/// blame the terminal. -/// -/// 2: the layout did not move, but what a frontend is ALLOWED TO ASK FOR did. -/// A frontend now clamps its window to `max_cols` x `max_rows` instead of -/// sending it raw (client.zig), and a 512x128 grid is a full frame a v1 daemon -/// PANICS encoding — its run length overflowed a u16 by exactly one cell, see -/// `run_max`. A v1 session refused that geometry outright, so nothing was ever -/// lost by refusing the connection instead; a v1 daemon meeting a v2 frontend -/// answers `Refusal.version`, which says so, rather than dying with every pane -/// shell it owns. This is the case the paragraph above is about: `zig build` -/// replaces the binary under a running session. pub const version: u16 = 2; -pub const Error = error{ - /// The message ended inside a field. - Truncated, - /// A length prefix, a run, or a grid dimension larger than this protocol - /// admits. Refused before anything is allocated or indexed. - Overlong, - /// A tag byte no version of this protocol has ever defined. - BadTag, - /// A tag this protocol does define, carrying a value it cannot mean: a - /// 3-in-a-bool, a zero-column resize, a pane past MAX_PANES. - BadValue, - /// The payload was decoded and bytes were left over. A message that says - /// more than its layout has room for is not this message. - Trailing, - /// The encoder ran out of caller-supplied buffer. - NoSpace, -}; - -// --------------------------------------------------------------------------- -// bounds -// --------------------------------------------------------------------------- +pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; -/// The largest grid this protocol carries. `Surface.cols`/`rows` are u16, so -/// these are protocol bounds rather than type bounds, and they exist because -/// `max_payload` below is derived from them: a decoder that accepts 65535 -/// columns accepts a 25 GiB frame prefix. The board's own grid is 56x14 and a -/// terminal's is usually near 200x50. -/// -/// NOT a ceiling above every real display, which is what this comment used to -/// claim: a 4K window at the SDL shell's minimum 8-pixel font is around 768 -/// columns by 216 rows, and a tty on the same screen passes 128 rows at any -/// ordinary line height. Those windows attach at 512x128 and letterbox the -/// rest (client.zig clamps), rather than being refused as they were. Raising -/// the pair instead would have been a bigger change than it looks: `frameBound` -/// stays well inside `max_payload`, but a themed full frame at 512x128 is -/// already ~0.9 MiB against server.zig's 1 MiB `out_backlog`. pub const max_cols: u16 = 512; pub const max_rows: u16 = 128; +pub const max_payload: u32 = 16 << 20; +pub const header_len = 5; // tag:u8, payload length:u32le -/// One cell at its largest: `default` false, a 7-byte grapheme, two rgb colors, -/// the attribute byte, the underline style and the font role. Written as the -/// sum of the fields rather than a number so that adding a field to `Cell` -/// moves it. const cell_max = 1 + 1 + 7 + 4 + 4 + 1 + 1 + 1; - -/// `start:u32 + count:u16`. A run's cost, and therefore the break-even the -/// encoder coalesces against (see `encodeFrame`). -const run_header = 4 + 2; - -/// ...and the longest run that `count:u16` can describe, which is EXACTLY ONE -/// SHORT of the largest grid this protocol carries: `max_cols * max_rows` is -/// 512*128 = 65536, and `maxInt(u16)` is 65535. -/// -/// A full frame of that grid is ONE run over all of it whenever the theme has -/// a background: `render` fills the surface and every pane then repaints its -/// text area, and `Surface.set` clears `default`, so `sendCell`'s `!default` -/// holds for every cell. (Under a theme with `bg = null` — `dark` — untouched -/// body cells stay default and a stretch of six of them breaks the run, so the -/// overflow was theme-dependent as well as geometry-dependent, which is the -/// worst kind of latent.) `encodeFrame`'s `@intCast(run_end - start)` then -/// panicked in a safe build and was illegal behaviour in a fast one — LLVM -/// happens to truncate to zero, which the far side refuses as `BadValue`, but -/// nothing promises that. That grid is what a frontend with a big window now -/// asks for (client.zig clamps to it), so the meeting point went from -/// unreachable to routine, and the encoder splits the run instead. +const run_header = 4 + 2; // start:u32, count:u16 const run_max = std.math.maxInt(u16); - -/// `kind:u8 + cols:u16 + rows:u16 + cursor(6) + nruns:u32`. const frame_head = 1 + 2 + 2 + 6 + 4; -/// The longest legal payload, and therefore the length prefix a decoder will -/// accept before it refuses the stream. Two messages set it: -/// * a full frame of the largest grid, worst case one run per cell: -/// 512*128 * (6 + 20) = 1.6 MiB. -/// * one paste, which the tty frontend already caps at 4 MiB (tty.zig -/// `max_paste_bytes`) on the grounds that anything larger is a mis-click. -/// 16 MiB is past every source file anyone edits in this editor and is still a -/// buffer the receiving side can simply hold. A larger message is not sent and -/// a larger prefix is not read. -pub const max_payload: u32 = 16 << 20; - -/// Every message is `tag:u8, len:u32le, payload[len]`. A u32 because a full -/// frame and a paste both pass 64 KiB; a u16 would have needed the frame split -/// across messages, which is a second framing layer for no gain. -pub const header_len = 5; - -/// Bytes `encodeFrame` may need for this grid, worst case: every cell changed, -/// every cell in a run of its own, every cell at `cell_max`. The server sizes -/// one buffer from this per geometry rather than guessing. +// Worst case: every cell changed, each in its own run. pub fn frameBound(cols: u16, rows: u16) usize { return header_len + frame_head + @as(usize, cols) * @as(usize, rows) * (run_header + cell_max); } -// --------------------------------------------------------------------------- -// tags -// --------------------------------------------------------------------------- - -/// Frontend -> core. Exhaustive on purpose, which is the opposite of -/// fuse.zig's `Opcode`: there, a newer KERNEL adds opcodes and a non-exhaustive -/// enum is the only way to receive one without undefined behaviour. Here both -/// ends are pardes and an unknown tag is not a newer peer — `version` already -/// refused that — so it is a corrupt or hostile stream and must be rejected. -/// `std.enums.fromInt` is how, at the one place a byte becomes a tag. -/// -/// The numbers are the PROTOCOL's, grouped session/input rather than derived -/// from `Event`'s declaration order, so reordering the union changes nothing. -/// -/// EVERY TAG HERE IS SOMETHING A HUMAN DID, and that is the whole set: a -/// handshake, a goodbye, and what a keyboard, a mouse, a trackpad or a window -/// manager produces. Six numbers are missing from the input run — 0x13..0x17 -/// and 0x1e — and the gaps are left rather than tidied away, because -/// renumbering is a change every deployed frontend feels. They were `output`, -/// `eof`, `lsp_resp`, `pipe_resp`, `file_changed` and `tick`: the -/// MACHINE-LOCAL host's own reports, which stopped being a frontend's business -/// when the daemon took the disk and the process table (host_io.zig, -/// file_watch.zig). No frontend ever produced one — tty.zig's attached loop -/// swallowed them by name and gui.zig never handed `Input.post` one — and -/// leaving them DECODABLE was not merely dead weight: server.zig's `apply` -/// routes any decoded non-resize event straight into `core.update`, so an -/// attached peer could forge a pane's output, forge an `eof` for a shell that -/// was still running (and unlike the daemon's own `paneEof` the wire path never -/// called `closePty`, so the master stayed open and the shell was orphaned for -/// the life of the session), or replace a pane's text with bytes the next -/// `Save` would write to disk. client.zig's header says a machine-local effect -/// cannot return to the wire; deleting these is what makes that true in BOTH -/// directions instead of only core -> frontend. +// Stable wire numbers; gaps are retired tags. Unknown tags are rejected by the decoder. pub const ClientTag = enum(u8) { hello = 0x01, bye = 0x02, @@ -237,23 +38,7 @@ pub const ClientTag = enum(u8) { pointer_leave = 0x1d, }; -/// Core -> frontend. 0x01..0x0f is the session; 0x10.. is one `push_` method -/// each, in `Host.VTable`'s own order so the two lists can be read side by -/// side. -/// -/// There are only THREE of those left, and which three is the whole design. -/// The daemon performs every effect that needs a disk or a process table -/// itself (see `host_io.zig`): a unix socket means it is on the same machine, -/// so there is no question of whose disk is meant, and a pane's shell has to -/// outlive the frontend that asked for it or a detached session is a promise -/// it cannot keep. What is left on the wire is what a process nobody is -/// looking at genuinely cannot do — put something on THIS human's clipboard, -/// read it back, and open a link in front of the person who clicked it. -/// -/// `detach` is in the SESSION range and not among those three on purpose: it is -/// not an effect the core wants performed, it is the session telling one -/// frontend that it is done. `quit` is its sibling — same shape, opposite -/// meaning about whether anything survives. +// Session control precedes display-local effects. pub const ServerTag = enum(u8) { welcome = 0x01, refuse = 0x02, @@ -880,15 +665,7 @@ fn sendCell(cells: []const pardes.Cell, prev: []const pardes.Cell, full: bool, i fn clientTag(msg: ClientMsg) ClientTag { return switch (msg) { .event => |ev| switch (ev) { - // SEVEN `Event`s a frontend cannot produce, so no `ClientTag` - // exists for them and this arm is where the compiler says so. - // `fs_req` is the acme mount, raised in the same process that - // answers it. The other six are the machine-local host's own - // reports — a pty's output and its EOF, a language or pipe worker's - // answer, a watched file's new bytes, an animation tick — and after - // the daemon took the disk and the process table every one of them - // is raised by the process that already holds the core. See - // `ClientTag` for what putting them back would let a peer forge. + // Machine-local reports and 9P requests belong to the session owner. .output, .eof, .lsp_resp, .pipe_resp, .file_changed, .tick, .fs_req => unreachable, inline else => |_, t| @field(ClientTag, @tagName(t)), }, |
