summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-26 13:27:46 -0300
committerGabriel Schneider <[email protected]>2026-08-27 09:47:39 -0300
commit11f380f6d7222f2cad93c2cdf13701ea1f903d47 (patch)
tree803194ee5853a6b4cda93f90a95e28d1f02e69ae /src/main.zig
parentfbc194068687e49a8490c85c9f1257a2f2bb9079 (diff)
downloadpardes-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.zig110
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