summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-27 15:32:02 -0300
committerGabriel Schneider <[email protected]>2026-08-27 16:12:35 -0300
commit5f4719da21f06b58694d52d354f5fda431ff8543 (patch)
treeeadd147cd18c53637f2287a3ed9e62fe1187280d /src/pardes.zig
parent0a21ea831d6791b406eef70b3e95cfd319d8ed36 (diff)
downloadpardes-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.zig32
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),