summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 15:53:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitb67dce15e662ae44de3286686f5a0afc147b918d (patch)
tree9f9dac843b61501bf778bf1d07ed7ee598b26db6 /src
parent2868137e34c973a47cbf667a431e885ca23cd9a2 (diff)
downloadpardes-b67dce15e662ae44de3286686f5a0afc147b918d.tar.gz
pardes-b67dce15e662ae44de3286686f5a0afc147b918d.zip
A REPL takes text only while its program has the terminal
A binding outlived its program: a command pane reused kept the REPL of the command before, a done command pane could be bound, a document pane (a terminal with no emulator) was accepted, and after Ctrl-D in python3 the text went to the shell, which ran import os as ImageMagick's. A command pane's binding now ends with its command and a done one is refused, a pane with no emulator is refused, and an interactive terminal's REPL is live only while a program other than its shell holds the tty (the host's tty_taken); when none of the language's REPLs is, nothing is sent or run and the pane says so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig5
-rw-r--r--src/exec.zig36
-rw-r--r--src/pardes.zig34
3 files changed, 66 insertions, 9 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 751d4026..a9bc38f9 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -914,7 +914,10 @@ pub const Repl = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
const pane = c.pane;
- if (!pane.isTerminal()) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL");
+ // A terminal with its emulator -- not a document pane -- and not
+ // a command pane whose command, the REPL it would be, is done.
+ if (!pane.isTerminal() or pane.terminal == null) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL");
+ if (pane.command != null and pane.command_done) return c.p.reportFailure(c.id, "Repl: its command is done; a REPL is a running program");
const arg = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
var buf: [320]u8 = undefined;
var idbuf: [48]u8 = undefined;
diff --git a/src/exec.zig b/src/exec.zig
index dbda7217..57eaad8f 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -394,10 +394,15 @@ pub fn langOf(pane: *const Pane) ?u8 {
return null;
}
-/// A bound REPL whose program can still take input: a command pane's is
-/// gone once its command is done.
-pub fn replLive(pane: *const Pane) bool {
- return pane.repl != null and pane.isTerminal() and !(pane.command != null and pane.command_done);
+/// A bound REPL whose program is there to take input: a command pane's
+/// while its command runs; an interactive terminal's while a program, not
+/// the shell, has its tty (the host's `tty_taken`) -- a python3 left with
+/// Ctrl-D leaves the shell, which would run `import os` as ImageMagick's.
+pub fn replLive(p: *const Pardes, id: usize) bool {
+ const pane = p.panes[id] orelse return false;
+ if (pane.repl == null) return false;
+ if (pane.command != null) return !pane.command_done;
+ return p.hostTtyTaken(id);
}
/// `python-a`.
@@ -408,22 +413,37 @@ pub fn replId(buf: []u8, pane: *const Pane) []const u8 {
const Routed = union(enum) { none, sent: usize, asked };
+/// The REPL bound here has no program running to take the text: said,
+/// and nothing sent or run.
+fn replGone(p: *Pardes, from: usize, repl: *const Pane) Routed {
+ var idbuf: [48]u8 = undefined;
+ var said: [128]u8 = undefined;
+ p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {s} has no program running; nothing sent", .{replId(&idbuf, repl)}) catch "Repl: no program running");
+ return .asked;
+}
+
/// Where an exec from `from`'s body goes when a REPL is bound for it: the
/// one there is, or a question when there are several.
fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed {
const src = p.panes[from] orelse return .none;
- if (src.repl != null and replLive(src)) {
+ if (src.repl != null) {
+ if (!replLive(p, from)) return replGone(p, from, src);
sendToRepl(p, from, from, text);
return .{ .sent = from };
}
const lang = langOf(src) orelse return .none;
var bound: [26]usize = undefined;
var n: usize = 0;
- for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and replLive(pane) and n < bound.len) {
+ var gone: ?usize = null;
+ for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and n < bound.len) {
+ if (!replLive(p, i)) {
+ gone = i;
+ continue;
+ }
bound[n] = i;
n += 1;
};
- if (n == 0) return .none;
+ if (n == 0) return if (gone) |i| replGone(p, from, p.panes[i].?) else .none;
if (n == 1) {
sendToRepl(p, from, bound[0], text);
return .{ .sent = bound[0] };
@@ -593,6 +613,8 @@ fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void {
pub fn commandDone(p: *Pardes, id: usize, status: ?u8) void {
const pane = p.panes[id] orelse return;
pane.command_done = true;
+ // A REPL it was is gone with its command; the pane's next is not one.
+ pane.repl = null;
pane.command_status = status;
pane.body.mode = .normal;
var buf: [16]u8 = undefined;
diff --git a/src/pardes.zig b/src/pardes.zig
index 429f33b9..f8328289 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1446,6 +1446,11 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe
return to[pane][0..len[pane]];
}
};
+ // The programs the REPLs run hold their terminals' ttys.
+ var host: FakeTtyQuery = .{};
+ host.install(p);
+ host.taken[1] = true;
+ host.taken[2] = true;
// Only a terminal is a REPL; a language's name is the syntax table's.
try std.testing.expect(p.executeBuiltinLine(0, "Repl python"));
try std.testing.expect(file.repl == null);
@@ -1503,6 +1508,18 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe
Sent.drain(p);
try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0);
+ // A REPL whose program has left (Ctrl-D: the shell has the tty) is
+ // sent nothing, and nothing runs: it says so.
+ host.taken[1] = false;
+ host.taken[2] = false;
+ p.exec_from_body = true;
+ _ = exec.execute(p, 0, "import os");
+ Sent.drain(p);
+ try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0 and !Sent.spawned);
+ try std.testing.expect(std.mem.indexOf(u8, file.msg[0..file.msg_len], "no program running") != null);
+ host.taken[1] = true;
+ host.taken[2] = true;
+
// Unbound, its letter is free again; bare, it says what it is.
try std.testing.expect(p.executeBuiltinLine(1, "Repl -"));
try std.testing.expect(p.panes[1].?.repl == null);
@@ -1510,6 +1527,21 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe
try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?));
try std.testing.expect(p.executeBuiltinLine(2, "Repl"));
try std.testing.expectEqualStrings("Repl: python-b", p.panes[2].?.msg[0..p.panes[2].?.msg_len]);
+
+ // A command pane is a REPL while its command runs: done, it is none,
+ // and a done one cannot be bound. A document pane is no terminal.
+ const cmd = p.freeSlot().?;
+ _ = try p.newCommand(cmd, "", "python3");
+ try std.testing.expect(p.executeBuiltinLine(cmd, "Repl python"));
+ try std.testing.expect(p.panes[cmd].?.repl != null);
+ p.update(.{ .exited = .{ .pane = @intCast(cmd), .status = 0 } });
+ try std.testing.expect(p.panes[cmd].?.repl == null);
+ try std.testing.expect(p.executeBuiltinLine(cmd, "Repl python"));
+ try std.testing.expect(p.panes[cmd].?.repl == null);
+ const doc_slot = p.freeSlot().?;
+ const doc = try p.newDocPane(doc_slot);
+ try std.testing.expect(p.executeBuiltinLine(doc_slot, "Repl python"));
+ try std.testing.expect(doc.repl == null);
}
test "Tty+fish, one word a tag can hold, opens a terminal on that shell" {
@@ -5172,7 +5204,7 @@ pub const Pardes = struct {
// Its letter sends it there; anything else takes it back.
if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') return;
for (p.panes, 0..) |target, to| if (target) |repl| if (repl.repl) |r|
- if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(repl))
+ if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(p, to))
return exec.sendToRepl(p, id, to, ask.text);
return;
}