From 2868137e34c973a47cbf667a431e885ca23cd9a2 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 15:49:45 -0300 Subject: 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 --- src/pardes.zig | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) (limited to 'src/pardes.zig') 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; -- cgit v1.3