summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 14:20:56 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commite7c181d6238dae4b08a02c1a9828917c2497f89d (patch)
tree299d4146c53ca5f882ba37cceb67a37302be2437 /src/host_io.zig
parenteb2428115588004a8bc04a09d45fb1104f727882 (diff)
downloadpardes-e7c181d6238dae4b08a02c1a9828917c2497f89d.tar.gz
pardes-e7c181d6238dae4b08a02c1a9828917c2497f89d.zip
A command line runs as its own command pane unless it is clicked at a shell's prompt
A middle click, an exec write or a tag word that no builtin knows was typed into some terminal for the pane's directory, sharing whatever state that shell was in and answering nothing, so a misspelling vanished into a shell. Now only a line clicked at an interactive terminal's prompt is typed there. From anywhere else it runs as a command pane: a terminal whose child is $SHELL -c the line in the pane's directory, full emulation, which shows its output and then exit N from the host's reaping of the child, and stays. A finished command pane is the next command's for its directory, which runs below what it showed after a '% line' line (acme appends to +Errors and never clears it, util.c:213); a running one gets a second pane. Kill ends a command pane's whole process group, the log records run and exit, exec reads back the command pane's serial, and a line is at most 1 KB, read off the pane as the host forks rather than carried in every spawn effect. ttyForDir's search for a free shell is gone. The goldens of chordcut, cmdword and layout-open change where a file's exec now opens a command pane, and ttytaken is rewritten to exec from the terminal itself; docs/open-questions.md records the decision. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig64
1 files changed, 61 insertions, 3 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 6009c00f..820cb2b6 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -949,6 +949,9 @@ test "the child environment replaces the launcher's terminal identity exactly on
extern "c" fn unsetenv(name: [*:0]const u8) c_int;
+/// The longest line a command pane runs (exec.zig refuses a longer one).
+pub const exec_command_max = pardes.exec.command_max;
+
pub fn forkShell(
core: ?*pardes.Pardes,
pane: usize,
@@ -973,9 +976,19 @@ pub fn forkShell(
};
var master: c_int = -1;
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
- const spawn = Shell.resolve(bin, &path_buf, prompt_rcs);
+ var spawn = Shell.resolve(bin, &path_buf, prompt_rcs);
+ // A command pane's child is the shell running its one line, which has
+ // no prompt to mark (exec.zig runCommand).
+ const one_line = if (core) |c| if (c.panes[pane]) |pn| pn.command else null else null;
+ var command_buf: [exec_command_max + 1]u8 = undefined;
+ if (one_line) |line| {
+ if (line.len > exec_command_max or std.mem.indexOfScalar(u8, line, 0) != null) return error.InvalidCommand;
+ @memcpy(command_buf[0..line.len], line);
+ command_buf[line.len] = 0;
+ spawn.argv = .{ spawn.path, "-c", command_buf[0..line.len :0].ptr, null };
+ }
var helper_buf: [4096]u8 = undefined;
- const mounted = if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false;
+ const mounted = one_line == null and if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false;
const helper: ?[:0]u8 = if (mounted) blk: {
if (comptime builtin.os.tag != .linux) return error.LinuxRequired;
if (fs == null) return error.SessionSocketRequired;
@@ -1016,7 +1029,7 @@ pub fn forkShell(
_exit(127);
}
ninep_io.setCloexec(master);
- if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), spawn.argv[1] != null);
+ if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), one_line == null and spawn.argv[1] != null);
return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid };
}
@@ -1349,6 +1362,29 @@ pub fn retireShell(pid: libc.pid_t) void {
thread.detach();
}
+/// A pane's child at its pty's end of file: reaped, and how it exited
+/// (its code, or 128 and the signal that ended it), if it is gone within
+/// `wait_ms`; null if it is still running (a job it left holds nothing
+/// open, but the child itself may be a moment behind its pty), which the
+/// caller then retires as it would any shell.
+pub fn exitStatus(pid: libc.pid_t, wait_ms: u32) ?u8 {
+ if (pid <= 0) return null;
+ var waited: u32 = 0;
+ while (true) : (waited += 2) {
+ var status: c_int = 0;
+ const got = libc.waitpid(pid, &status, libc.W.NOHANG);
+ if (got == pid) {
+ const s: u32 = @bitCast(status);
+ if (libc.W.IFEXITED(s)) return @truncate(libc.W.EXITSTATUS(s));
+ if (libc.W.IFSIGNALED(s)) return @truncate(128 + @as(u32, @intFromEnum(libc.W.TERMSIG(s))));
+ return null;
+ }
+ if (got < 0 or waited >= wait_ms) return null;
+ const ts: libc.timespec = .{ .sec = 0, .nsec = 2 * std.time.ns_per_ms };
+ _ = libc.nanosleep(&ts, null);
+ }
+}
+
fn reapShell(pid: libc.pid_t, grace_ms: u32) void {
var waited: u32 = 0;
while (waited < grace_ms) : (waited += 5) {
@@ -1711,6 +1747,28 @@ test "Kill stops the foreground job, never the shell, and finds none with job co
try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG));
}
+test "a command pane's child is its shell running the one line, and its exit status is read at the end" {
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const id = core.freeSlot().?;
+ _ = try core.newCommand(id, "", "printf 'said '; exit 7");
+ while (core.nextEffect()) |_| {}
+ const rcs: Shell.PromptFiles = .{};
+ const child = try forkShell(core, id, &rcs, "/bin/sh", "", 24, 80, null);
+ defer _ = libc.close(child.file.handle);
+ var got: [256]u8 = undefined;
+ var n: usize = 0;
+ while (n < got.len) {
+ const r = libc.read(child.file.handle, got[n..].ptr, got.len - n);
+ if (r <= 0) break; // EIO once the child is gone: the pty's end
+ n += @intCast(r);
+ }
+ try std.testing.expect(std.mem.indexOf(u8, got[0..n], "said") != null);
+ try std.testing.expectEqual(@as(?u8, 7), exitStatus(child.pid, 2000));
+}
+
test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {
const pid = libc.fork();
if (pid == 0) {