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/host.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/host.zig')
| -rw-r--r-- | src/host.zig | 345 |
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"), - }; - } -}; |
