summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig67
1 files changed, 58 insertions, 9 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index aa39cf32..5bc1ea6d 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -816,8 +816,17 @@ const Pty = struct {
kill_at: i64 = 0,
reader: ?std.Thread = null,
stop: [2]c_int = .{ -1, -1 },
+ /// A command pane's child, watched to its exit (host_io.watchExit);
+ /// the pty stays open until both its end of file and its exit.
+ cmd: host_io.CommandWatch = .{},
};
+/// A watched child exited: wake the loop, which takes it (`takeExits`).
+fn wakeForExit(ctx: ?*anyopaque) void {
+ const q: *Queue = @ptrCast(@alignCast(ctx.?));
+ q.wake();
+}
+
const RetiredShell = struct { pid: libc.pid_t = 0, kill_at: i64 = 0 };
const Msg = union(enum) {
@@ -1298,7 +1307,7 @@ test "GUI PTY Restore joins a real reader waiting for queue space" {
try std.testing.expect(host_io.writeFd(child.file.handle, "printf 'after-full'; exit\n"));
try PtyTests.waitBlocked(&queue);
const old_serial = core.panes[0].?.serial;
- const replacement = try dump.restore(core, core.dump_out.?);
+ const replacement = try dump.restore(core, core.dump_out.?, "test");
shell.stopPtys();
core.deinit();
core = replacement;
@@ -1627,7 +1636,7 @@ test "GUI LSP worker limit rejects before allocation and recovers after owned wo
try std.testing.expect(core.lsp_wait == null);
try std.testing.expectEqualStrings("abc", pane.file.?.content);
try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col);
- try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "WorkersBusy") != null);
+ try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "workers busy") != null);
try std.testing.expectEqual(@as(usize, 0), failing.alloc_index);
try std.testing.expect(!failing.has_induced_failure);
try std.testing.expectEqual(rejected, queue.lsp_id.?);
@@ -2617,6 +2626,8 @@ fn localSession(
shell.threads_ok = true;
pardes.lsp.setStatusSink(&queue, lspStatusSink);
defer pardes.lsp.setStatusSink(null, null);
+ host_io.setExitWake(&queue, wakeForExit);
+ defer host_io.setExitWake(null, null);
while (!core.quit) {
pardes.turn.restoreSettled();
@@ -2642,7 +2653,7 @@ fn localSession(
break :blk;
};
defer gpa.free(bytes);
- const nc = dump.restore(core, bytes) catch |err| {
+ const nc = dump.restore(core, bytes, rp) catch |err| {
core.reportError(core.active, "Restore", err);
break :blk;
};
@@ -2942,6 +2953,8 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
shell.threads_ok = true;
pardes.lsp.setStatusSink(&queue, lspStatusSink);
defer pardes.lsp.setStatusSink(null, null);
+ host_io.setExitWake(&queue, wakeForExit);
+ defer host_io.setExitWake(null, null);
setStdinRaw() catch {}; // stdin may be a pipe, not a pty — best effort
@@ -3999,8 +4012,20 @@ const Shell = struct {
break :vt v;
};
+ /// The command panes' exits, told once their output is in (host_io).
+ fn takeExits(s: *Shell) void {
+ host_io.takeExits(s.core, s.ptys, s, closeWatched);
+ }
+
+ fn closeWatched(s: *Shell, id: usize) void {
+ s.closePty(@intCast(id));
+ s.saw_event = true;
+ }
+
fn drainQueue(s: *Shell) void {
s.reconcilePtys();
+ // After this batch's output, which an exit is told behind.
+ defer s.takeExits();
var msgs = s.queue.take();
var check_files = false;
for (msgs.slice()) |m| switch (m) {
@@ -4011,14 +4036,36 @@ const Shell = struct {
s.saw_event = true;
},
.eof => |e| {
- if (s.gens[e.pane] == e.gen) {
- s.closePty(e.pane);
+ if (s.gens[e.pane] == e.gen) eof: {
if (e.failure) |err| {
+ s.closePty(e.pane);
if (s.core.panes[e.pane]) |pane| {
pane.body.mode = .normal;
s.core.reportError(e.pane, "terminal reader", err);
}
- } else s.core.update(.{ .eof = .{ .pane = e.pane } });
+ break :eof;
+ }
+ const pt = if (s.ptys[e.pane]) |*pt| pt else {
+ s.core.update(.{ .eof = .{ .pane = e.pane } });
+ break :eof;
+ };
+ // A command's pty stays open until its child has exited:
+ // closing it would hang up one that runs on without it.
+ // Its exit, if it came first, is told now its output is in.
+ if (pt.cmd.watched) {
+ host_io.commandEof(s.core, s.ptys, e.pane, s, closeWatched);
+ break :eof;
+ }
+ // Unwatched, a command's exit is read here, as its end.
+ const unwatched = if (s.core.panes[e.pane]) |pane| pane.command != null else false;
+ var status: ?u8 = null;
+ if (unwatched) {
+ status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
+ }
+ s.closePty(e.pane);
+ if (unwatched) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
+ s.core.update(.{ .eof = .{ .pane = e.pane } });
}
s.saw_event = true;
},
@@ -4325,7 +4372,8 @@ fn spawnPane(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void {
if (s.ptys[pane] != null) return s.core.reportError(pane, "shell", error.WorkersBusy);
s.gens[pane] +%= 1;
const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err);
- const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial };
+ const command = s.core.panes[pane].?.command != null;
+ const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } };
s.ptys[pane] = pt;
var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined;
if (host_io.shellCwd(pt.pid, &lbuf)) |wd| s.core.setCwd(pane, wd);
@@ -4353,7 +4401,8 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const s = shellOf(ctx);
- if (s.ptys[pane]) |pt| if (pt.fd >= 0) host_io.signalTty(pt.pid, pt.fd, sig);
+ // A command whose exit is recorded is reaped: its group may be another's.
+ if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
@@ -4491,7 +4540,7 @@ test "the headless grid host round-trips a yank back as a paste" {
core.update(.{ .key = .{ .cp = ' ' } });
core.update(.{ .key = .{ .cp = 'y' } });
while (core.nextEffect()) |e| core.perform(e);
- const yanked = core.yank orelse return error.MissingYank;
+ const yanked = core.registers.text(core.gpa, '+') orelse return error.MissingYank;
try std.testing.expect(yanked.len > 0);
const before = pane.file.?.content.len;