diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-27 15:32:02 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-27 16:12:35 -0300 |
| commit | 5f4719da21f06b58694d52d354f5fda431ff8543 (patch) | |
| tree | eadd147cd18c53637f2287a3ed9e62fe1187280d /src/pardes.zig | |
| parent | 0a21ea831d6791b406eef70b3e95cfd319d8ed36 (diff) | |
| download | pardes-5f4719da21f06b58694d52d354f5fda431ff8543.tar.gz pardes-5f4719da21f06b58694d52d354f5fda431ff8543.zip | |
acmefs: a pane's terminal gets pty/data, pty/ctl and pty/status
Step 3 of the 9P chain (docs/9p.typ 12.3, docs/registry.typ 9P-8). Nothing here
is about 9P: it lands in the FUSE-served tree and any later transport inherits it.
A script could write into a terminal that already existed and read its rendered
scrollback. It could not START one, RESIZE one or SIGNAL one. Two of those were
already effects the core emits, so `exec` and `winsize` are existing
capabilities acquiring a name; only `sig` is new, and it brings the one new
host method, `push_pty_signal`.
pty/ctl winsize <cols> <rows> | sig INT|TERM|HUP|QUIT|KILL | exec
one verb per line, validate-all then apply-all, EINVAL applies
nothing -- `writeCtl`'s shape and `writeCtl`'s reason
pty/status cols, rows, tty-taken as three %11d fields
pty/data write is input to the process; read is the RAW output stream,
gated on a reader count so a pane nobody reads costs one branch
A pane that is not a terminal has no pty/ at all: the lookup is ENOENT and
readdir does not list it.
`PaneFile` is an enum(u4) and this takes it from 11 values to 15. ONE REMAINS.
That is also why pty/ is a DIRECTORY and not three more flat names -- a
subdirectory costs one value and buys its own namespace, so `ctl` and `data`
did not have to be renamed.
Two things the core does not know, and which are therefore not invented: a
child's EXIT STATUS (a shell's death is `Event.eof`, which removes the pane,
so there is no directory left to read it in) and RAW/COOKED (the core never
sets a termios; the mode belongs to the program on the far side).
Verified live against a daemon: pty/ appears only on the terminal pane; a
`winsize 0 24` and a `sig SIGINT` are refused; a bad verb beside a good one
applies neither; `echo pty-works` written to pty/data runs in the shell and its
output reaches the body; and a blocking read of pty/data returns the raw stream,
OSC 133 marks and all. fs-bench unchanged and still zero allocations.
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 32 |
1 files changed, 30 insertions, 2 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 55461c5d..111e2853 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3809,12 +3809,27 @@ pub const AttachRequest = struct { pane: u8, name: []const u8 }; /// freed the moment `off` reaches the end or the pane goes away. pub const PendingWrite = struct { bytes: []u8, off: usize = 0 }; +/// WHAT `pty/ctl`'s `sig` VERB CAN SEND. Named rather than numeric because the +/// core is freestanding: it has no `SIGINT` to name and a number written here +/// would be one platform's number travelling to a host that may not share it. +/// Five, and deliberately not the whole of signal(7): these are the ones a +/// human at a terminal already has a key or a `kill` for, and every one of +/// them means something to a program on a tty. `sig USR1` at a shell is a +/// message to a daemon, not a terminal operation, and nothing asked for it. +pub const PtySignal = enum(u8) { int, term, hup, quit, kill }; + /// IO the core wants done. Payloads are inline (fixed buffers): effects are /// queued values with no lifetime ties back into the core. pub const Effect = union(enum) { spawn: struct { pane: u8, cwd: Buf(256) }, write: struct { pane: u8, bytes: Buf(64) }, resize_pty: struct { pane: u8, cols: u16, rows: u16 }, + /// Deliver a signal to whatever is on this pane's tty — `pty/ctl`'s + /// `sig INT`, i.e. the ^C a script cannot type because ^C is not a byte + /// the pty would interpret on its own. The only genuinely new capability + /// the `pty/` directory added: `spawn` and `resize_pty` above were already + /// here, so `exec` and `winsize` are those two acquiring a name. + signal_pty: struct { pane: u8, sig: PtySignal }, open_link: Buf(256), /// write this pane's file content to its path; the shell reads both off /// the core (content is unbounded, effects are fixed-size values) @@ -6805,8 +6820,11 @@ pub const Pardes = struct { } /// Has a program taken this pane's tty? A host that cannot tell says no, - /// which is how pardes behaved before the probe existed. - fn hostTtyTaken(p: *const Pardes, id: usize) bool { + /// which is how pardes behaved before the probe existed. Public because + /// `pty/status` reports it: it is the one field of that file the core does + /// not own itself, and asking here rather than reaching for the vtable in + /// acmefs keeps the null-method default in one place. + pub fn hostTtyTaken(p: *const Pardes, id: usize) bool { const f = p.host.vtable.pull_tty_taken orelse return false; return f(p.host.ctx, @intCast(id)); } @@ -6857,6 +6875,10 @@ pub const Pardes = struct { // screen, and the bytes are dropped rather than transcribed. .write => |w| if (v.push_pty_write) |f| f(p.host.ctx, w.pane, w.bytes.slice()), .resize_pty => |r| if (v.push_pty_resize) |f| f(p.host.ctx, r.pane, r.cols, r.rows), + // A host with no signal method has no child to signal: the + // fallback host's ptys are silent (see `.write` above), so there + // is nothing to record and nothing to lie about. + .signal_pty => |s| if (v.push_pty_signal) |f| f(p.host.ctx, s.pane, s.sig), .open_link => |u| if (v.push_open_link) |f| f(p.host.ctx, u.slice()) else @@ -7081,6 +7103,12 @@ pub const Pardes = struct { }, .output => |o| { const pane = p.panes[o.pane] orelse return; + // The RAW bytes, before the emulator eats them. `pty/data`'s + // read is the only thing that wants them — the grid is a + // rendering and cannot be un-rendered — and this is one load + // and one branch on a pane nobody is reading. See + // `acmefs.notePtyOutput`. + acmefs.notePtyOutput(p, o.pane, o.bytes); term_pane.feedOutput(p, pane, o.bytes); }, .eof => |e| p.removePane(e.pane), |
