diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 15:49:45 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 2868137e34c973a47cbf667a431e885ca23cd9a2 (patch) | |
| tree | 2756597f564038f22f61d77f00598a6c996fcd05 | |
| parent | 5570377c4997a5abe9f433bc15c927bcde237dc1 (diff) | |
| download | pardes-2868137e34c973a47cbf667a431e885ca23cd9a2.tar.gz pardes-2868137e34c973a47cbf667a431e885ca23cd9a2.zip | |
Tty with a shell that is not there says so, and never sets the caller's shell
Tty /nonexistent or Tty fsh started the host's fallback shell without a word,
and when spawnTty made no pane, the argument became the calling pane's shell.
Tty now looks the shell up first and fails 'Tty: no shell "fsh"' when it is
neither a name on the usual paths nor a path to one; spawnTty answers the pane
it made, the one given the shell, and says why when it made none. The lookup
(host_io Shell.find) gives the turn up around its access() calls, since a
typed path may be under a mount this editor serves, and forkShell copies the
pane's shell before looking it up and checks the pane is still its own after.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | src/builtins.zig | 17 | ||||
| -rw-r--r-- | src/exec.zig | 21 | ||||
| -rw-r--r-- | src/host_io.zig | 25 | ||||
| -rw-r--r-- | src/pardes.zig | 25 | ||||
| -rw-r--r-- | test/panes.zig | 2 |
5 files changed, 64 insertions, 26 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index fbab925c..751d4026 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -874,11 +874,18 @@ pub const Tty = struct { pub const takes_arg = true; pub const plus_arg = true; pub fn run(c: Ctx) void { - exec.spawnTty(c.p, c.id); - const arg = c.arg orelse return; - const pane = c.p.panes[c.p.active] orelse return; - if (!pane.isTerminal() or pane.shell != null) return; - pane.shell = c.p.gpa.dupe(u8, arg) catch return; + const arg = std.mem.trim(u8, c.arg orelse "", " \t"); + // A shell that is not there is said, where the host would quietly + // start its fallback in its place. + if (arg.len > 0) { + var buf: [std.fs.max_path_bytes]u8 = undefined; + if (comptime pardes.hosted) if (@import("host_io.zig").Shell.find(arg, &buf) == null) { + var said: [320]u8 = undefined; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Tty: no shell \"{s}\" (a name on the usual paths, or a path to one)", .{arg[0..@min(arg.len, 200)]}) catch "Tty: no such shell"); + }; + } + const pane = exec.spawnTty(c.p, c.id) orelse return; + if (arg.len > 0) pane.shell = c.p.gpa.dupe(u8, arg) catch null; } }; diff --git a/src/exec.zig b/src/exec.zig index cc2d3c4f..dbda7217 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -292,8 +292,8 @@ fn askWrite(p: *Pardes, id: usize, serial: u32, path: []const u8) void { /// Tty: a shell in the caller's directory, raw from the first frame, /// stacked below the caller like Alt-n's new shell. -pub fn spawnTty(p: *Pardes, from: usize) void { - spawnTtyWithMount(p, from, false); +pub fn spawnTty(p: *Pardes, from: usize) ?*Pane { + return spawnTtyWithMount(p, from, false); } pub fn spawnV9fsTty(p: *Pardes, from: usize) void { @@ -301,13 +301,19 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void { return p.reportError(from, "Tty9p", error.LinuxRequired); if (p.fs.socket_path.len == 0) return p.reportError(from, "Tty9p", error.SessionSocketRequired); - spawnTtyWithMount(p, from, true); + _ = spawnTtyWithMount(p, from, true); } -fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { - const src = p.panes[from] orelse return; - const free = p.freeSlot() orelse return; - const nt = p.newShell(free, Pardes.paneDir(src)) catch return; +fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { + const src = p.panes[from] orelse return null; + const free = p.freeSlot() orelse { + p.reportFailure(from, "Tty: no pane free"); + return null; + }; + const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| { + p.reportError(from, "Tty", err); + return null; + }; nt.greet = !v9fs; nt.v9fs_on_spawn = v9fs; nt.body.mode = .tty; @@ -317,6 +323,7 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { layout.splitBelow(p, parent, nt); p.active = free; p.stillborn_joiner = nt.serial; + return nt; } /// when a doc lands in `col`, a lone pristine shell there is clutter — drop diff --git a/src/host_io.zig b/src/host_io.zig index 0f6fd87e..68d35a9c 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -743,8 +743,13 @@ pub const Shell = struct { return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } }; } - fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 { + /// A shell by its path or its name in the usual directories, or null. + /// A path may be under a mount this editor serves: the turn goes out + /// with the lookups (pardes.Turn.yield). + pub fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 { if (bin.len == 0 or bin.len + 1 > buf.len) return null; + pardes.turn.yield(); + defer pardes.turn.back(); if (std.mem.indexOfScalar(u8, bin, '/') != null) { @memcpy(buf[0..bin.len], bin); buf[bin.len] = 0; @@ -975,14 +980,22 @@ pub fn forkShell( if (stat.kind != .directory) return error.NotDir; break :dir path; }; - // The turn was given up for the stat: a 9P client may have closed the - // pane, and another taken its slot, whose command this is not. - if (core) |c| if ((if (c.panes[pane]) |pn| pn.serial else 0) != serial) return error.PaneGone; var master: c_int = -1; var path_buf: [std.fs.max_path_bytes]u8 = undefined; - // A terminal opened on a shell of its own (`Tty fish`) runs that one. - const own = if (core) |c| if (c.panes[pane]) |pn| pn.shell else null else null; + // A terminal opened on a shell of its own (`Tty fish`) runs that one, + // copied: looking it up gives the turn up, and the pane may go. + var own_buf: [256]u8 = undefined; + var own: ?[]const u8 = null; + if (core) |c| if (c.panes[pane]) |pn| if (pn.shell) |name| { + const n = @min(name.len, own_buf.len); + @memcpy(own_buf[0..n], name[0..n]); + own = own_buf[0..n]; + }; var spawn = Shell.resolve(own orelse bin, &path_buf, prompt_rcs); + // The turn was given up for the stat and the lookup: a 9P client may + // have closed the pane, and another taken its slot, whose command this + // is not. + if (core) |c| if ((if (c.panes[pane]) |pn| pn.serial else 0) != serial) return error.PaneGone; // 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; diff --git a/src/pardes.zig b/src/pardes.zig index 58354b95..429f33b9 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1193,7 +1193,7 @@ test "first document fallback splits like Tty from the originating pane" { const free = p.freeSlot().?; const doc = try p.openTutorView(free); exec.placeDoc(p, from, free, doc); - exec.spawnTty(tty, from); + _ = exec.spawnTty(tty, from); p.sync(); tty.sync(); try std.testing.expectEqual(@as(usize, 2), p.ncol); @@ -1516,11 +1516,22 @@ test "Tty+fish, one word a tag can hold, opens a terminal on that shell" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); while (p.nextEffect()) |_| {} - try std.testing.expect(p.executeBuiltinLine(0, "Tty+fish")); + // sh, the one shell every host has. + try std.testing.expect(p.executeBuiltinLine(0, "Tty+sh")); try std.testing.expect(p.active != 0); - try std.testing.expectEqualStrings("fish", p.panes[p.active].?.shell.?); - p.acknowledgeShell(p.active, "/usr/bin/fish", true); - try std.testing.expect(std.mem.startsWith(u8, tagline.curTail(p.panes[p.active].?), " Tty+fish ")); + try std.testing.expectEqualStrings("sh", p.panes[p.active].?.shell.?); + p.acknowledgeShell(p.active, "/bin/sh", false); + try std.testing.expect(std.mem.startsWith(u8, tagline.curTail(p.panes[p.active].?), " Tty+sh ")); + p.active = 0; + // A shell that is not there is said, not quietly replaced, and the + // pane that asked gets no shell of its own. + const before = p.active; + const caller = p.panes[0].?; + try std.testing.expect(p.executeBuiltinLine(0, "Tty /nonexistent")); + try std.testing.expect(p.active == before and caller.shell == null); + try std.testing.expect(std.mem.indexOf(u8, caller.msg[0..caller.msg_len], "no shell \"/nonexistent\"") != null); + try std.testing.expect(p.executeBuiltinLine(0, "Tty fsh-not-a-shell")); + try std.testing.expect(caller.shell == null); // Only a word that says so splits at `+`: `Dump+x.zon` is no Dump. try std.testing.expect(!p.executeBuiltinLine(0, "Dump+x.zon")); try std.testing.expect(!p.executeBuiltinLine(0, "Msg+hello")); @@ -7059,7 +7070,7 @@ test "raw tty Escape only leaves at a shell prompt" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24, .file = "mise.toml" }); defer p.deinit(); - exec.spawnTty(p, 0); + _ = exec.spawnTty(p, 0); const shell = p.active; const pane = p.panes[shell].?; var buf: [256]u8 = undefined; @@ -7090,7 +7101,7 @@ test "raw tty Ctrl-B switches to editor mode without sending child input" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .file = "mise.toml" }); defer p.deinit(); - exec.spawnTty(p, 0); + _ = exec.spawnTty(p, 0); const shell = p.active; p.sync(); var buf: [256]u8 = undefined; diff --git a/test/panes.zig b/test/panes.zig index d0551cfc..662ea3a5 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -920,7 +920,7 @@ const TtySelectionTests = struct { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 24 }); defer p.deinit(); p.presentation.enabled = false; - pardes.exec.spawnTty(p, 0); + _ = pardes.exec.spawnTty(p, 0); for (p.panes[0..2]) |slot| { slot.?.body.mode = .tty; slot.?.tty_filter = false; |
