summaryrefslogtreecommitdiff
path: root/src/host.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/host.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-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/host.zig')
-rw-r--r--src/host.zig345
1 files changed, 0 insertions, 345 deletions
diff --git a/src/host.zig b/src/host.zig
deleted file mode 100644
index 2c6bd208..00000000
--- a/src/host.zig
+++ /dev/null
@@ -1,345 +0,0 @@
-//! THE HOST SEAM: everything the core cannot do itself, as one struct of
-//! OPTIONAL function pointers — `std.mem.Allocator`/`std.Io` shape, and the
-//! generalization of two vtables this codebase already grew on its own
-//! (`pardes.TtyQuery`, and the macOS shell's `Runtime`).
-//!
-//! Every method is optional, and a null method is not an error: the core
-//! substitutes a default backed by ordinary data structures in this process
-//! (`Fallback` below). So a host implements only what it actually has, and the
-//! core cannot tell the difference — a `Save` lands in a real file under the
-//! tty host and in `Fallback.files` under a host that never wrote a filesystem
-//! method, and every path above that behaves identically.
-//!
-//! Two consequences worth having on purpose:
-//! * The zero-method host IS the test harness. A `Host{}` is a complete,
-//! deterministic, in-process pardes with a virtual filesystem, a virtual
-//! clipboard and silent ptys.
-//! * `Fallback` lives on the Pardes instance, not here, so N cores driven by
-//! one fan-out host each keep their own state and can run in parallel.
-//!
-//! WHAT IS NOT HERE, and why: whether a capability EXISTS in this build stays
-//! comptime and stays next to the code it shapes (`pardes.platform`,
-//! `pardes.pdf_enabled`, `builtins.capabilities`, `PdfSlot`/`HapticSlot`).
-//! A vtable cannot make a field zero-sized or a builtin absent from an enum.
-//! The rule is: comptime decides what a BUILD has, this vtable decides who
-//! SERVES it at runtime.
-const std = @import("std");
-const pardes = @import("pardes.zig");
-const source_manifest = @import("source_manifest.zig");
-
-pub const LspRequest = struct {
- id: u32,
- kind: pardes.lsp.Kind,
- pane: u8,
- offset: u32,
- arg: []const u8,
-};
-
-pub const Host = struct {
- ctx: ?*anyopaque = null,
- vtable: *const VTable = &.{},
-
- /// One optional method per thing a host can do. Adding a method here is
- /// additive for every existing host: they keep it null and get the default.
- ///
- /// EVERY name says how a fan-out must route it, and the compiler enforces
- /// that it does (see Fanout.isPull):
- /// `push_` every wrapped host gets it, and it returns nothing — a push
- /// with an answer would have N answers and no way to pick one.
- /// `pull_` exactly ONE host serves it, because there is one of whatever
- /// comes back: one value, one sleep that ends, one `Event.paste`
- /// for one Ctrl-V, one `lsp_resp` per request id.
- pub const VTable = struct {
- // ---- the loop's own three seams ----
- /// Block until there is input or `timeout_ms` elapses, translating
- /// whatever arrives into `Pardes.update`/`postEvent` calls. This is the
- /// ONLY place the process is allowed to sleep: the core never spins.
- /// A pull because one host does the sleeping — fanned out, the second
- /// host would not be serviced until the first happened to wake.
- pull_wait_input: ?*const fn (ctx: ?*anyopaque, timeout_ms: u32) void = null,
- push_present: ?*const fn (ctx: ?*anyopaque, surface: *const pardes.Surface) void = null,
- /// After the frame is on screen (panel-presentation acknowledgement,
- /// pointer refresh); split from `push_present` because it must observe
- /// a frame the user has actually seen.
- push_post_present: ?*const fn (ctx: ?*anyopaque) void = null,
- /// Per-frame host bookkeeping with no event of its own: cwd polling, a
- /// capability handshake landing, gamepad state.
- push_poll_frame: ?*const fn (ctx: ?*anyopaque) void = null,
-
- // ---- this frontend's own membership ----
- /// `Detach` — leave the session, which carries on for everybody else.
- /// Only a DETACHED core's host implements it, and the null case is the
- /// point rather than an oversight: a local tty or SDL shell has no
- /// session to leave, so the core reports that on the pane's row (see
- /// `perform`) instead of quietly quitting something. Argumentless like
- /// the two frame pushes above, because the host serving it already
- /// knows whose keystroke arrived — it is the one that delivered it.
- push_detach: ?*const fn (ctx: ?*anyopaque) void = null,
-
- // ---- pseudo-terminals ----
- push_spawn: ?*const fn (ctx: ?*anyopaque, pane: u8, cwd: []const u8) void = null,
- push_pty_write: ?*const fn (ctx: ?*anyopaque, pane: u8, bytes: []const u8) void = null,
- push_pty_resize: ?*const fn (ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void = null,
- /// Deliver a signal to whatever is on this pane's tty — `pty/ctl`'s
- /// `sig INT`. A PUSH because there is no answer to have: `kill(2)`
- /// either reaches a process that is already gone or reaches one whose
- /// disposition the sender cannot see, and a script that wants to know
- /// whether the program died reads the pane. NULL means this host owns
- /// no pane shells and therefore has no child to signal — the browser
- /// and the board, where the same null already makes `push_spawn` and
- /// `push_pty_write` silent — and the effect is dropped exactly as a
- /// write to a pane with no pty is.
- push_pty_signal: ?*const fn (ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void = null,
- /// Is this pane's terminal still the prompt the host forked, or has a
- /// program (vim, a pager, an agent) taken its tty? An effect cannot
- /// answer it — the `execute` that asks must choose a destination inside
- /// its own update, and effects drain after. A pushed fact would mean
- /// every host probing every pane's processes every frame to answer a
- /// question asked when a human middle-clicks a word. So the host leaves
- /// a way to be asked and the core asks where it decides. The answer
- /// must not re-enter the core.
- pull_tty_taken: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null,
-
- // ---- the board's own pads ----
- /// Flip one GPIO and report the level it held and the level it now holds. False means the
- /// host would not do it: a pin number outside the part, or no pads at all.
- ///
- /// A pull, because there is one answer. The HOST answers it rather than the core reaching
- /// for the registers itself - which `Peek` and `Poke` do two functions away - because
- /// driving a pad correctly is not one register. It is the IO MUX function select, the GPIO
- /// matrix output route, the pad's drive and input-buffer bits, and the output enable, keyed
- /// by a per-pin table. The firmware already owns that code and checks it against ESP-IDF's
- /// own headers on the die; a second copy in here would be a second copy nobody tests.
- pull_gpio_toggle: ?*const fn (ctx: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool = null,
-
- // ---- the filesystem ----
- /// `pane` travels with the bytes only so a host that posts a "saved"
- /// message row can name the right pane; the core already resolved the
- /// path and the content, so save_file and save_text both land here.
- ///
- /// A HOST THAT COULD NOT WRITE MUST CALL `Pardes.saveFailed`, and the
- /// reason it is a call rather than a return value is the rule twenty
- /// lines below: a `push_` reaches every host in a fan-out, so there is
- /// no single answer to give back. The core marks the pane saved
- /// optimistically around this call and `saveFailed` takes it back, so a
- /// write that could not happen — a read-only file, a directory removed
- /// under the pane, a full disk — leaves the ` *` in the tag where it
- /// was. Until that existed the pane came clean on a save that never
- /// happened, and `Del` makes no dirty check: the edits were one click
- /// from gone with the screen saying they were safe.
- push_write_file: ?*const fn (ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void = null,
- /// The session dump. Separate because the host also chooses WHERE it
- /// goes (dump.outPath is libc-bound; the freestanding core cannot).
- push_write_dump: ?*const fn (ctx: ?*anyopaque, bytes: []const u8) void = null,
- push_watch_file: ?*const fn (ctx: ?*anyopaque, pane: u8, path: []const u8, on: bool) void = null,
- push_watch_theme: ?*const fn (ctx: ?*anyopaque, generation: u32, on: bool) void = null,
- push_dump_themes: ?*const fn (ctx: ?*anyopaque, pane: u8) void = null,
-
- // ---- the desktop ----
- push_set_clipboard: ?*const fn (ctx: ?*anyopaque, text: []const u8) void = null,
- /// Ask; the answer arrives later as an ordinary `Event.paste`, which is
- /// why this returns nothing and is still a pull: two hosts answering
- /// would paste the clipboard twice. Null answers immediately from the
- /// in-process clipboard instead, so a request never goes unanswered.
- pull_read_clipboard: ?*const fn (ctx: ?*anyopaque) void = null,
- push_open_link: ?*const fn (ctx: ?*anyopaque, url: []const u8) void = null,
-
- // ---- work that must leave the loop ----
- /// Both answer exactly once, keyed by the id they carry, so both are
- /// pulls: a second host's reply would arrive for a request already
- /// completed and the core would apply it to whatever holds that id now.
- pull_lsp: ?*const fn (ctx: ?*anyopaque, req: LspRequest) void = null,
- pull_pipe: ?*const fn (ctx: ?*anyopaque, id: u32) void = null,
- /// Hand one filesystem answer back to whoever asked for it (a FUSE
- /// `write(2)` to /dev/fuse). `bytes` is the payload the core resolved
- /// for this reply and is borrowed for the length of this call — it may
- /// point straight into a pane's text, so a host that needs it later
- /// copies it. A push and not a pull: the answer is already computed,
- /// and a second host serving the same mount is not a thing that
- /// happens (the transport that asked is the one holding the request).
- push_fs_reply: ?*const fn (ctx: ?*anyopaque, reply: *const pardes.acmefs.Reply, bytes: []const u8) void = null,
- };
-};
-
-/// Where a host with no `write_dump` puts a session dump. Named here so the
-/// core writes it and reports it as one path.
-pub const fallback_dump_path = "pardes.dump.zon";
-
-/// The in-process implementations behind every null method: a virtual
-/// filesystem, a virtual clipboard, and a record of what was asked of ptys and
-/// the desktop. Ordinary data structures, one set per Pardes instance.
-///
-/// The filesystem is not empty. It is pardes's own source, embedded — see
-/// source_manifest.zig — with `files` holding only what this session WROTE, so
-/// a Save shadows the built-in copy and reading it back returns the edit. That
-/// is what makes a host with no file methods a usable pardes rather than one
-/// staring at an empty buffer.
-///
-/// Only `files` grows, and it grows by REPLACING a path's content, so no
-/// session accumulates. A pane whose child does not exist is SILENT: its bytes
-/// are dropped rather than transcribed, because nothing reads a transcript back
-/// and a browser session would then carry every keystroke forever.
-pub const Fallback = struct {
- gpa: std.mem.Allocator,
- files: std.StringHashMapUnmanaged([]u8) = .empty,
- clipboard: std.ArrayListUnmanaged(u8) = .empty,
- /// Last link a host with no browser was asked to open.
- link: std.ArrayListUnmanaged(u8) = .empty,
- spawned: [pardes.MAX_PANES]bool = @splat(false),
- watched: [pardes.MAX_PANES]bool = @splat(false),
-
- pub fn deinit(f: *Fallback) void {
- var it = f.files.iterator();
- while (it.next()) |e| {
- f.gpa.free(e.key_ptr.*);
- f.gpa.free(e.value_ptr.*);
- }
- f.files.deinit(f.gpa);
- f.clipboard.deinit(f.gpa);
- f.link.deinit(f.gpa);
- }
-
- /// True when the bytes are in the map. The core turns a false into the same
- /// `saveFailed` a real host reports, so a virtual filesystem that could not
- /// allocate does not leave a pane looking saved either.
- pub fn writeFile(f: *Fallback, path: []const u8, bytes: []const u8) bool {
- const copy = f.gpa.dupe(u8, bytes) catch return false;
- if (f.files.getEntry(path)) |e| {
- f.gpa.free(e.value_ptr.*);
- e.value_ptr.* = copy;
- return true;
- }
- const key = f.gpa.dupe(u8, path) catch {
- f.gpa.free(copy);
- return false;
- };
- f.files.put(f.gpa, key, copy) catch {
- f.gpa.free(key);
- f.gpa.free(copy);
- return false;
- };
- return true;
- }
-
- /// What this path holds now: the session's own write, else the embedded
- /// source. Borrowed — the bytes live in the map or in the binary.
- pub fn get(f: *const Fallback, path: []const u8) ?[]const u8 {
- if (f.files.get(path)) |written| return written;
- return source_manifest.find(path);
- }
-
- pub fn setClipboard(f: *Fallback, text: []const u8) void {
- f.clipboard.clearRetainingCapacity();
- f.clipboard.appendSlice(f.gpa, text) catch {};
- }
-
- pub fn setLink(f: *Fallback, url: []const u8) void {
- f.link.clearRetainingCapacity();
- f.link.appendSlice(f.gpa, url) catch {};
- }
-};
-
-/// Fan out one core's host calls to several real hosts at once — the debugging
-/// arrangement: every input reaches every host, and each host answers into its
-/// own state.
-///
-/// It advertises a method only when some wrapped host actually implements it,
-/// so wrapping does NOT mask the core's per-method fallback: fan out two hosts
-/// that never opened a link and the link still lands in `Fallback`.
-pub const Fanout = struct {
- hosts: []const Host,
- vt: Host.VTable = .{},
-
- pub fn init(hosts: []const Host) Fanout {
- var f: Fanout = .{ .hosts = hosts };
- inline for (@typeInfo(Host.VTable).@"struct".fields) |field| {
- for (hosts) |h| if (@field(h.vtable, field.name) != null) {
- @field(f.vt, field.name) = @field(all, field.name);
- break;
- };
- }
- return f;
- }
-
- pub fn host(f: *const Fanout) Host {
- return .{ .ctx = @ptrCast(@constCast(f)), .vtable = &f.vt };
- }
-
- fn self(ctx: ?*anyopaque) *const Fanout {
- return @ptrCast(@alignCast(ctx.?));
- }
-
- /// A wrapper for every method, whether or not this fan-out advertises it.
- /// Synthesized, so adding a method to `Host.VTable` needs no code here.
- const all: Host.VTable = blk: {
- var t: Host.VTable = .{};
- for (@typeInfo(Host.VTable).@"struct".fields) |field| {
- @field(t, field.name) = fan(field.name);
- }
- break :blk t;
- };
-
- fn Method(comptime name: []const u8) std.builtin.Type.Fn {
- const ptr = @typeInfo(@FieldType(Host.VTable, name)).optional.child;
- return @typeInfo(@typeInfo(ptr).pointer.child).@"fn";
- }
-
- /// How to route a method, read off its own name. A method that is neither
- /// is a COMPILE ERROR rather than a silent push, because the failure of a
- /// forgotten pull is invisible in every unit test and obvious only to the
- /// user: one Ctrl-V pasting twice.
- fn isPull(comptime name: []const u8) bool {
- if (std.mem.startsWith(u8, name, "pull_")) return true;
- if (std.mem.startsWith(u8, name, "push_")) {
- if (Method(name).return_type.? != void) @compileError("Host.VTable." ++
- name ++ " reaches every host, so it cannot return a value: whose answer would it be?");
- return false;
- }
- @compileError("Host.VTable." ++ name ++ " must be named push_… (every host gets it) " ++
- "or pull_… (exactly one host serves it, because there is one of whatever comes back)");
- }
-
- /// The walk, written once: `args` is everything after `ctx`.
- fn dispatch(comptime name: []const u8, ctx: ?*anyopaque, args: anytype) Method(name).return_type.? {
- for (self(ctx).hosts) |h| if (@field(h.vtable, name)) |fp| {
- const answer = @call(.auto, fp, .{h.ctx} ++ args);
- if (comptime isPull(name)) return answer;
- };
- // `init` installs a wrapper only when some host has the method, so a
- // pull always found one; a zero is the honest answer if that changes.
- const R = Method(name).return_type.?;
- if (comptime R != void) return std.mem.zeroes(R);
- }
-
- /// One wrapper, built from the method's own signature: the parameter list
- /// is the only part that cannot be derived, so there is one shape per
- /// arity rather than one per method.
- fn fan(comptime name: []const u8) @FieldType(Host.VTable, name) {
- const m = Method(name);
- const R = m.return_type.?;
- const P = m.params;
- return switch (P.len) {
- 1 => struct {
- fn w(c: ?*anyopaque) R {
- return dispatch(name, c, .{});
- }
- }.w,
- 2 => struct {
- fn w(c: ?*anyopaque, a: P[1].type.?) R {
- return dispatch(name, c, .{a});
- }
- }.w,
- 3 => struct {
- fn w(c: ?*anyopaque, a: P[1].type.?, b: P[2].type.?) R {
- return dispatch(name, c, .{ a, b });
- }
- }.w,
- 4 => struct {
- fn w(c: ?*anyopaque, a: P[1].type.?, b: P[2].type.?, d: P[3].type.?) R {
- return dispatch(name, c, .{ a, b, d });
- }
- }.w,
- else => @compileError("Fanout has no wrapper shape for " ++ name ++ "'s arity"),
- };
- }
-};