diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-26 13:27:46 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-27 09:47:39 -0300 |
| commit | 11f380f6d7222f2cad93c2cdf13701ea1f903d47 (patch) | |
| tree | 803194ee5853a6b4cda93f90a95e28d1f02e69ae /src/main.zig | |
| parent | fbc194068687e49a8490c85c9f1257a2f2bb9079 (diff) | |
| download | pardes-11f380f6d7222f2cad93c2cdf13701ea1f903d47.tar.gz pardes-11f380f6d7222f2cad93c2cdf13701ea1f903d47.zip | |
One core behind N frontends, the board's own runner moved in, and every 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.
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 110 |
1 files changed, 101 insertions, 9 deletions
diff --git a/src/main.zig b/src/main.zig index c60fb888..08beae86 100644 --- a/src/main.zig +++ b/src/main.zig @@ -85,6 +85,19 @@ const help_text = \\ PARDES_FS and PARDES_PANE into every pane shell \\ --fs=<dir> ...at <dir> instead. Must be absolute; pardes \\ unmounts it on exit but leaves the directory + \\ --detach run this session with NO terminal of its own, + \\ serving frontends over a unix socket beside the + \\ nested-instance one. The core, the panes and the + \\ undo history outlive every frontend that attaches + \\ --detach=<name> ...named <name> rather than this process's pid, so + \\ a frontend can say which session it wants. One + \\ path component: no '/' and nothing empty + \\ --attach become a frontend of the one detached session + \\ that is running: draw its screen, send it input, + \\ fork its pane shells. Several frontends may be + \\ attached at once and all see the same screen + \\ --attach=<name> ...of the session called <name>, which is what to + \\ use when more than one is running \\ -h, --help show this help and exit \\ ; @@ -128,14 +141,27 @@ fn nativeMain(init: std.process.Init) !void { // bare `pardes` boots straight into tty mode — and so does a `pardes` // carrying nothing but flags that say something about the SESSION rather // than about its layout: --nested is about this session's relationship to - // its parent, --fs is about who may script it, and neither says anything - // about what should be on screen. Anything else (a FILE, -n, --tty) is - // layout, and answers this question itself further down. + // its parent, --fs is about who may script it, --detach is about who may + // WATCH it, --attach is about whose screen this one is showing, and none of + // the four says anything about what should be on screen. Anything else (a + // FILE, -n, --tty) is layout, and answers this question itself further + // down. opts.tty_only = for (args[1..]) |a| { if (!std.mem.eql(u8, a, "--nested") and !std.mem.eql(u8, a, "--fs") and - !std.mem.startsWith(u8, a, "--fs=")) break false; + !std.mem.startsWith(u8, a, "--fs=") and + !std.mem.eql(u8, a, "--detach") and + !std.mem.startsWith(u8, a, "--detach=") and + !std.mem.eql(u8, a, "--attach") and + !std.mem.startsWith(u8, a, "--attach=")) break false; } else true; + // `--detach[=<name>]`: null when it was not given, so the empty string is + // free to mean "the default name" the way opts.fs uses it for a directory. + var detach: ?[]const u8 = null; + // ...and `--attach[=<name>]`, the same shape: null when it was not given, + // and the empty string means "the one session there is" (tty.zig + // `sessionName`) rather than a session with no name. + var attach: ?[]const u8 = null; // Kept RAW until every flag is parsed: classifying it means chdir'ing into // a directory and recording nothing, and the nested client below still // needs the word itself to resolve. @@ -171,6 +197,22 @@ fn nativeMain(init: std.process.Init) !void { opts.fs = a["--fs=".len..]; } else if (std.mem.eql(u8, a, "--nested")) { opts.nested = true; + } else if (std.mem.eql(u8, a, "--detach")) { + detach = ""; + } else if (std.mem.startsWith(u8, a, "--detach=")) { + // `--detach=<name>` and never `--detach <name>`, for exactly the + // reason --fs gives above: the flag is useful bare, so a two-word + // form would make `pardes --detach README` a session called README + // that opens no file. + detach = a["--detach=".len..]; + } else if (std.mem.eql(u8, a, "--attach")) { + attach = ""; + } else if (std.mem.startsWith(u8, a, "--attach=")) { + // `--attach=<name>` and never `--attach <name>`, for the reason + // --fs states above and --detach repeats: the flag is useful bare, + // so a two-word form would make `pardes --attach README` an attach + // to a session called README that opens no file. + attach = a["--attach=".len..]; } else if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) { try std.Io.File.stdout().writeStreamingAll(init.io, help_text); return; @@ -186,7 +228,17 @@ fn nativeMain(init: std.process.Init) !void { // instance resolves against ITS panes' directories, which are not ours. // A word naming nothing on disk sends nothing and falls through to the // classification below, which already refuses it — no second UI either way. - if (!opts.nested) if (nested.outer()) |outer_pid| { + // + // `--detach` is exempt for the same reason `--nested` is, arrived at from + // the other side: it stacks no UI at all. A detached session started from a + // pane is a session, not a request that the outer instance open something, + // and handing it our positional would leave the caller with no session. + // + // `--attach` is exempt for the mirror of that: it stacks a UI, but the UI + // is a session that already exists somewhere else, and handing our word to + // the outer instance would open the file in the WRONG session and leave + // the caller with no frontend. + if (!opts.nested and detach == null and attach == null) if (nested.outer()) |outer_pid| { const word = positional orelse { try std.Io.File.stderr().writeStreamingAll(init.io, nested_text); std.process.exit(1); @@ -228,14 +280,41 @@ fn nativeMain(init: std.process.Init) !void { opts.startup_config = found.bytes; opts.startup_config_path = found.path; opts.config_dir = found.dir; + // `--detach` is the core with no terminal and `--attach` is a terminal + // with no core, so the two together are a contradiction with no useful + // reading. Refused rather than resolved by declaration order, which would + // silently drop whichever flag lost. + if (detach != null and attach != null) return error.BadArgs; + // `--detach` replaces the frontend rather than choosing among them: the + // core runs here, with no terminal, and the frontends are elsewhere on a + // socket (src/detached/). It is checked before `platform` because it is not + // a shell — the tty and gui builds can both be asked for one. + if (detach) |name| { + // Bare `--detach` is named by this process's pid, which is the one name + // nobody has to be told and no two sessions can share. Unsigned: `{d}` + // prints a leading '+' for a positive SIGNED int, which is nested.zig's + // note about the same cast. + const named = if (name.len != 0) + name + else + try std.fmt.allocPrint(arena, "{d}", .{@as(u32, @intCast(std.c.getpid()))}); + return @import("detached/server.zig").run(init, opts, named); + } + // A frontend is a SHELL, and only the tty one knows how to be one today. + // Comptime-folded, so a tty build carries none of this. + if (attach != null and pardes.platform != .tty) { + try std.Io.File.stderr().writeStreamingAll(init.io, "pardes: --attach needs the tty shell\n"); + std.process.exit(1); + } switch (pardes.platform) { - .tty => try @import("tty/tty.zig").run(init, opts), + .tty => try @import("tty/tty.zig").run(init, opts, attach), .gui => try @import("gui/gui.zig").run(init, opts), // Every other shell is entered by its host and never links this file // at all: the browser through src/web.zig, the macOS app through - // src/macos.zig, and the ESP32-P4 firmware through its own app root in - // the zig-p4 package, which imports this package's `pardes_p4` module. - .web, .macos, .p4 => unreachable, + // src/macos.zig, and the ESP32-P4 firmware through src/esp32p4/app.zig, + // which is a root of its own in this repository and links the + // `pardes-esp32p4` object over the C ABI in src/esp32p4.zig. + .web, .macos, .esp32p4 => unreachable, } } @@ -277,6 +356,19 @@ test { // silence. That is the fonts.zig story above, told once already. _ = @import("acmefs.zig"); _ = @import("fuse.zig"); + // The detached-session transport (src/detached/), same story as fuse.zig + // above: it speaks the core's Event/Surface, so it belongs in THIS module + // rather than a standalone b.addTest, and nothing the core analyses reaches + // it. A TTY build does — tty.zig imports client.zig for `--attach` — but + // these names are what makes the transport's tests exist in every other + // build too, and `--detach` is not a tty-only feature. + // client.zig's own tests drive a real `Session` over a real socket, so + // naming it reaches server.zig too — but server.zig is named anyway, for + // the acmefs.zig reason: the day client.zig stops importing it is the day + // those tests vanish in silence. + _ = @import("detached/wire.zig"); + _ = @import("detached/server.zig"); + _ = @import("detached/client.zig"); if (comptime pardes.platform == .tty) { _ = @import("tty/tty.zig"); // tty.zig calls the compositor only from its runtime loop, so merely |
