summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 14:59:17 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitd70f0029f0808e0a3724150a081015e0f943dfb5 (patch)
tree32cd92b54c52f2e6d9c6fec6d5ef42e34af7b9e4 /src/pardes.zig
parentada81f05127506dec0dbb7af390cb43fd0da9d40 (diff)
downloadpardes-d70f0029f0808e0a3724150a081015e0f943dfb5.tar.gz
pardes-d70f0029f0808e0a3724150a081015e0f943dfb5.zip
A command pane's command is over when its process exits, not when its pty closes
A job left in the background (sleep 100 &) held the pty open, so the pane stayed running and the child a zombie until the job ended; a command that closed its terminal and ran on got its end of file at once, the host waited 100 ms for an exit, reported exit ? and hung it up. Now each command's child is watched on a thread (waitid with WNOWAIT, so its pid stays its own until the host reaps it), and the host tells the core the exit from that: after the pty's end of file, so the output before the exit is in, or 50 ms after the exit without one, a job holding the pty. The pty stays open until both, so a command that let go of its terminal is never hung up by it. All four front ends; a host that cannot start the watcher reads the exit at end of file as before. Tests: host_io's for both cases, and cmdexit.snap end to end. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig18
1 files changed, 14 insertions, 4 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 97ba534a..1539637c 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1425,7 +1425,10 @@ test "a command pane shows how its command ended, and the next command there run
try std.testing.expect(other != dst);
while (p.nextEffect()) |_| {}
p.update(.{ .output = .{ .pane = @intCast(dst), .bytes = "compiled\r\n" } });
- p.update(.{ .eof = .{ .pane = @intCast(dst), .status = 2 } });
+ // Its output ending is not its end: that is its child's exit.
+ p.update(.{ .eof = .{ .pane = @intCast(dst) } });
+ try std.testing.expect(p.panes[dst] == pane and !pane.command_done);
+ p.update(.{ .exited = .{ .pane = @intCast(dst), .status = 2 } });
try std.testing.expect(p.panes[dst] == pane);
try std.testing.expect(pane.command_done);
try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, pane), "(make -j8) exit 2"));
@@ -3134,9 +3137,13 @@ pub const Event = union(enum) {
row_metrics: ?RowMetrics = null,
},
output: struct { pane: u8, bytes: []const u8 },
- /// The pane's child hung up its pty; `status` is how it exited, when
- /// the host could reap it at once (a command pane shows it).
+ /// The pane's pty reached end of file: its output is over. An
+ /// interactive terminal closes; a command pane waits for `exited`.
eof: struct { pane: u8, status: ?u8 = null },
+ /// A command pane's child exited, with this status (null: unknown). It
+ /// is the command's end, whatever still holds its pty (a job it left in
+ /// the background prints on until end of file).
+ exited: struct { pane: u8, status: ?u8 },
lsp_resp: struct { id: u32, rows: ?[]const u8 },
pipe_resp: struct {
id: u32,
@@ -4869,8 +4876,11 @@ pub const Pardes = struct {
panes.Terminal.feedOutput(p, pane, o.bytes);
ctlfs.pty.noteMarks(p, o.pane, pane);
},
- .eof => |e| if (p.panes[e.pane]) |pane| if (pane.command != null) {
+ .exited => |e| if (p.panes[e.pane]) |pane| if (pane.command != null and !pane.command_done) {
exec.commandDone(p, e.pane, e.status);
+ },
+ .eof => |e| if (p.panes[e.pane]) |pane| if (pane.command != null) {
+ // Output is over; the command is over when its child exits.
} else p.removePane(e.pane, null) catch |err| {
pane.body.mode = .normal;
p.reportError(e.pane, "terminal exited; Del retries close", err);