diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 15:53:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | b67dce15e662ae44de3286686f5a0afc147b918d (patch) | |
| tree | 9f9dac843b61501bf778bf1d07ed7ee598b26db6 /src | |
| parent | 2868137e34c973a47cbf667a431e885ca23cd9a2 (diff) | |
| download | pardes-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.zig | 5 | ||||
| -rw-r--r-- | src/exec.zig | 36 | ||||
| -rw-r--r-- | src/pardes.zig | 34 |
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; } |
