summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 03:44:26 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit69f8e4703f6c1204f9f0958f264d2a4c4600026f (patch)
tree01bab7f3d7cea8a557780c0e1263962752892732
parent29ae6460961f4f781088455b72946260113a9ad9 (diff)
downloadpardes-69f8e4703f6c1204f9f0958f264d2a4c4600026f.tar.gz
pardes-69f8e4703f6c1204f9f0958f264d2a4c4600026f.zip
pty/run's busy names the program holding the terminal
busy said only that something ran. Where the host can tell (Linux, the tty's foreground group's /proc comm, a new fg_name host call), it says busy: <program> is running; elsewhere, busy as before. The GUI's host gains the one small callback (render agent's file: gui.zig, fgName). Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/detached/server.zig9
-rw-r--r--src/gui/gui.zig8
-rw-r--r--src/host_io.zig15
-rw-r--r--src/ninep/pty.zig7
-rw-r--r--src/pardes.zig5
-rw-r--r--src/tty/tty.zig7
-rw-r--r--test/fs.py3
7 files changed, 52 insertions, 2 deletions
diff --git a/src/detached/server.zig b/src/detached/server.zig
index af299051..bb66b841 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -373,6 +373,7 @@ pub const Session = struct {
.pty_signal = ptySignal,
.close_pty = closePaneShell,
.tty_taken = ttyTaken,
+ .fg_name = fgName,
.kill_job = killJob,
.write_file = writeFile,
.write_dump = writeDump,
@@ -544,6 +545,14 @@ pub const Session = struct {
return host_io.ttyTaken(pt.pid, pt.fd);
}
+ fn fgName(ctx: ?*anyopaque, pane: u8, buf: []u8) ?[]const u8 {
+ const s = of(ctx);
+ if (pane >= s.ptys.len) return null;
+ const pt = s.ptys[pane];
+ if (pt.fd < 0) return null;
+ return host_io.foregroundName(pt.pid, pt.fd, buf);
+ }
+
fn killJob(ctx: ?*anyopaque, pane: u8) bool {
const s = of(ctx);
if (pane >= s.ptys.len) return false;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 491373a8..de5da1e7 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -4045,6 +4045,7 @@ const Shell = struct {
.pty_signal = ptySignal,
.close_pty = closePaneShell,
.tty_taken = ttyTaken,
+ .fg_name = fgName,
.kill_job = killJob,
.write_file = writeFile,
.write_dump = writeDump,
@@ -4507,6 +4508,13 @@ fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
return host_io.ttyTaken(pt.pid, pt.fd);
}
+fn fgName(ctx: ?*anyopaque, pane: u8, buf: []u8) ?[]const u8 {
+ const s = shellOf(ctx);
+ const pt = s.ptys[pane] orelse return null;
+ if (pt.fd < 0) return null;
+ return host_io.foregroundName(pt.pid, pt.fd, buf);
+}
+
fn killJob(ctx: ?*anyopaque, pane: u8) bool {
const s = shellOf(ctx);
const pt = s.ptys[pane] orelse return false;
diff --git a/src/host_io.zig b/src/host_io.zig
index 10a85d8f..1db0dac9 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -25,6 +25,9 @@ pub const Host = struct {
pty_signal: ?*const fn (ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void = null,
close_pty: ?*const fn (ctx: ?*anyopaque, pane: u8) void = null,
tty_taken: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null,
+ /// The name of the program holding the tty, when it is not the
+ /// shell (host_io.foregroundName), in `buf`.
+ fg_name: ?*const fn (ctx: ?*anyopaque, pane: u8, buf: []u8) ?[]const u8 = null,
/// Answered at once, not queued as an effect: Kill asks while the
/// job it saw running is still the one in the tty (host_io.killJob).
kill_job: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null,
@@ -1751,6 +1754,18 @@ pub fn killJob(shell_pid: libc.pid_t, master_fd: c_int) bool {
return libc.kill(-fg, libc.SIG.TERM) == 0;
}
+/// The command name of the tty's foreground job, when that is not the
+/// shell: what `pty/run` says is running. Linux only (/proc/<pgrp>/comm).
+pub fn foregroundName(shell_pid: libc.pid_t, master_fd: c_int, buf: []u8) ?[]const u8 {
+ if (comptime builtin.os.tag != .linux) return null;
+ const fg = tcgetpgrp(master_fd);
+ if (fg <= 0 or fg == shell_pid) return null;
+ var name: [64:0]u8 = undefined;
+ const path = std.fmt.bufPrintSentinel(&name, "/proc/{d}/comm", .{@as(u32, @intCast(fg))}, 0) catch return null;
+ const comm = std.mem.trimEnd(u8, readProc(path, buf) orelse return null, "\n");
+ return if (comm.len > 0) comm else null;
+}
+
fn readProc(path: [*:0]const u8, buf: []u8) ?[]const u8 {
const fd = libc.open(path, .{ .ACCMODE = .RDONLY });
if (fd < 0) return null;
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 393516ce..6b84af61 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -189,7 +189,12 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
// marks: a nested shell's prompt (ssh, a shell with its own
// integration) looks like an empty prompt to ghostty but is not the
// shell this run knows.
- answer(p, slot, "busy", .{});
+ // Naming what runs, where the host can tell (Linux).
+ var name: [64]u8 = undefined;
+ if (p.hostForegroundName(id, &name)) |program|
+ answer(p, slot, "busy: {s} is running", .{program})
+ else
+ answer(p, slot, "busy", .{});
} else {
// Sent now if the shell is at its prompt, or at its first prompt
// when it has not drawn one yet (noteMarks).
diff --git a/src/pardes.zig b/src/pardes.zig
index c103b9d7..d6859f86 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5124,6 +5124,11 @@ pub const Pardes = struct {
return ev;
}
+ pub fn hostForegroundName(p: *const Pardes, id: usize, buf: []u8) ?[]const u8 {
+ const f = p.host.vtable.fg_name orelse return null;
+ return f(p.host.ctx, @intCast(id), buf);
+ }
+
pub fn hostTtyTaken(p: *const Pardes, id: usize) bool {
const f = p.host.vtable.tty_taken orelse return false;
return f(p.host.ctx, @intCast(id));
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 754b4b96..e7eddec4 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -946,6 +946,7 @@ const Shell = struct {
.pty_signal = ptySignal,
.close_pty = closePty,
.tty_taken = ttyTaken,
+ .fg_name = fgName,
.kill_job = killJob,
.write_file = writeFile,
.write_dump = writeDump,
@@ -1295,6 +1296,12 @@ const Shell = struct {
return host_io.ttyTaken(pt.pid, pt.file.handle);
}
+ fn fgName(ctx: ?*anyopaque, pane: u8, buf: []u8) ?[]const u8 {
+ const s = of(ctx);
+ const pt = s.ptys[pane] orelse return null;
+ return host_io.foregroundName(pt.pid, pt.file.handle, buf);
+ }
+
/// The command panes' exits, told once their output is in (host_io).
fn takeExits(s: *@This()) void {
host_io.takeExits(s.core, &s.ptys, s, closeWatched);
diff --git a/test/fs.py b/test/fs.py
index bac7020c..b97c6967 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -428,7 +428,8 @@ def run_file(binary):
waiter = threading.Thread(target=lambda: slow.append(run(other, term, b'sleep 1\n')), daemon=True)
waiter.start()
time.sleep(.3)
- assert run(client, term, b'true\n') == b'busy\n'
+ # The job the other client ran holds the tty, and says so.
+ assert run(client, term, b'true\n') == b'busy: sleep is running\n'
waiter.join(5)
assert slow == [b'exit 0\n'], slow
closing = []