From 4793520998969439bb569c968c3143ae46aefc66 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 22:17:49 -0300 Subject: A pane's question is logged as ask, and answer on its ctl answers it A script could not answer the several-REPLs question, which only a key could. Every question a pane asks on its notice band (which REPL takes an exec, Del's side from the keyboard) is now logged as `ask `, and `answer ` on the pane's ctl answers it as that key would, `answer -` taking it back as Esc does. One function answers for a key and for the ctl. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 2 ++ src/exec.zig | 5 +++ src/ninep/ctl.zig | 8 +++++ src/ninep/testing.zig | 9 +++++ src/pardes.zig | 91 ++++++++++++++++++++++++++++++++++++++++----------- 5 files changed, 96 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/builtins.zig b/src/builtins.zig index 72df0271..1db010af 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -854,6 +854,8 @@ pub const Del = struct { { c.pane.prompt = .del_side; c.p.active = c.id; + var rec: [32]u8 = undefined; + pardes.ctlfs.events.notePath(c.p, "ask", std.fmt.bufPrint(&rec, "{d} del k j", .{c.pane.serial}) catch ""); // The question says Del already. if (c.pane.msg_announcement) c.pane.msg_len = 0; return; diff --git a/src/exec.zig b/src/exec.zig index 0c0930e2..2611162b 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -461,6 +461,11 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { src.question_len = @intCast(w.buffered().len); src.prompt = .repl_choice; p.active = from; + var rec: [96]u8 = undefined; + var rw = std.Io.Writer.fixed(&rec); + rw.print("{d} repl", .{src.serial}) catch {}; + for (bound[0..n]) |i| rw.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {}; + pardes.ctlfs.events.notePath(p, "ask", rw.buffered()); return .asked; } diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index edf81cd9..8ac60ec5 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -506,6 +506,14 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { if (!held) return tree.failText(req.tag, E.INVAL, e_not_locked); held = false; if (apply) pane.fs.lock = null; + } else if (std.mem.startsWith(u8, line, "answer ")) { + // The pane's question (its `ask` in the log), answered. + const choice = std.mem.trim(u8, line["answer ".len..], " "); + if (choice.len != 1) return refuse(p, req, "answer takes one choice, or -", line); + const id = p.paneBySerial(serial).?; + if (p.panes[id].?.prompt != .del_side and p.panes[id].?.prompt != .repl_choice) + return refuse(p, req, "no question asked", line); + if (apply) _ = p.answerQuestion(id, if (choice[0] == '-') null else choice[0]); } else if (!apply) { if (checkBuiltin(p, req, line, .pane)) |refusal| return refusal; } else if (runBuiltin(p, req, p.paneBySerial(serial).?, line)) |refusal| { diff --git a/src/ninep/testing.zig b/src/ninep/testing.zig index fce77f10..927552e2 100644 --- a/src/ninep/testing.zig +++ b/src/ninep/testing.zig @@ -56,6 +56,15 @@ pub fn rd(p: *Pardes, node: u64, off: u64, size: u32) Answer { return call(p, .{ .tag = 1, .op = .read, .node = node, .off = off, .size = size }); } +/// Whether the log, read whole from a fresh open, holds `needle`. +pub fn logHas(p: *Pardes, needle: []const u8) bool { + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + defer _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); + const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; + return std.mem.indexOf(u8, text, needle) != null; +} + pub fn wr(p: *Pardes, node: u64, data: []const u8) Answer { return call(p, .{ .tag = 2, .op = .write, .node = node, .data = data }); } diff --git a/src/pardes.zig b/src/pardes.zig index e20eb17e..8a6fa589 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1514,6 +1514,16 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe Sent.drain(p); try std.testing.expectEqualStrings("print(2)\r", Sent.said(2)); try std.testing.expect(Sent.len[1] == 0 and file.prompt == .none); + // The question is in the log, and a script answers it on the ctl. + { + const th = @import("ninep/testing.zig"); + _ = exec.executeFrom(p, 0, "print(5)", true); + var want: [32]u8 = undefined; + try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} repl a b\n", .{file.serial}))); + const answered = th.wr(p, ctlfs.Node.of(file.serial, .ctl), "answer a\n"); + try std.testing.expectEqual(ctlfs.Status.ok, answered.reply.status); + try std.testing.expectEqualStrings("print(5)\r", answered.pty()); + } _ = exec.executeFrom(p, 0, "print(3)", true); p.update(.{ .key = .{ .cp = Key.escape } }); Sent.drain(p); @@ -5295,6 +5305,42 @@ pub const Pardes = struct { } } + /// Answers the question pane `id` asks on its notice band, logged as + /// `ask `: Del's side (`k` above, `j` below) or + /// the REPL to send to (its letter). Null, or a choice not offered, + /// takes the question back. A key answers it, or `answer` on the pane's + /// ctl; false when the pane asks nothing. + pub fn answerQuestion(p: *Pardes, id: usize, choice: ?u8) bool { + const asked = p.panes[id] orelse return false; + switch (asked.prompt) { + .repl_choice => { + asked.prompt = .none; + const ask = p.repl_ask orelse return true; + p.repl_ask = null; + defer p.gpa.free(ask.text); + const letter = choice orelse return true; + if (letter < 'a' or letter > 'z') return true; + const serial = ask.offered[letter - 'a']; + if (serial == 0) return true; + // The pane offered, still that REPL and still running: it + // may have closed or been rebound while the question stood. + if (p.paneBySerial(serial)) |to| if (p.panes[to].?.repl) |r| + if (r.lang == ask.lang and r.letter == letter and exec.replLive(p, to)) { + exec.sendToRepl(p, id, to, ask.text); + return true; + }; + p.setMessage(id, "Repl: the REPL chosen is gone; nothing sent"); + }, + .del_side => { + asked.prompt = .none; + if (choice == 'k') exec.runBuiltin(p, .Del, id, "", "k"); + if (choice == 'j') exec.runBuiltin(p, .Del, id, "", "j"); + }, + else => return false, + } + return true; + } + fn handleKey(p: *Pardes, key: Key) void { // A double-click is two clicks with nothing between them. p.last_click = null; @@ -5303,27 +5349,12 @@ pub const Pardes = struct { // pane or a header edit is in: that key is its answer. for (p.panes, 0..) |slot, id| { const asked = slot orelse continue; - if (asked.prompt == .repl_choice) { - asked.prompt = .none; - const ask = p.repl_ask orelse return; - p.repl_ask = null; - defer p.gpa.free(ask.text); + switch (asked.prompt) { // 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; - const serial = ask.offered[key.cp - 'a']; - if (serial == 0) return; - // The pane offered, still that REPL and still running: it - // may have closed or been rebound while the question stood. - if (p.paneBySerial(serial)) |to| if (p.panes[to].?.repl) |r| - if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(p, to)) - return exec.sendToRepl(p, id, to, ask.text); - p.setMessage(id, "Repl: the REPL chosen is gone; nothing sent"); - return; + .repl_choice => _ = p.answerQuestion(id, if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') null else @intCast(key.cp)), + .del_side => _ = p.answerQuestion(id, if (hit(key, config.del_above)) 'k' else if (hit(key, config.del_below)) 'j' else null), + else => continue, } - if (asked.prompt != .del_side) continue; - asked.prompt = .none; - if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k"); - if (hit(key, config.del_below)) exec.runBuiltin(p, .Del, id, "", "j"); return; } const pane = p.panes[p.active] orelse return; @@ -6754,6 +6785,28 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expectEqual(top, p.active); try std.testing.expectEqual(h[2], p.rects[low].h); + // A script answers the question on the pane's ctl, as the log's `ask` + // names it: `answer -` takes it back, a side closes the pane toward it. + { + const th = @import("ninep/testing.zig"); + p.newScratchBelow(top); + const asked = p.active; + p.sync(); + const serial = p.panes[asked].?.serial; + const ctl = ctlfs.Node.of(serial, .ctl); + try std.testing.expectEqual(ctlfs.E.INVAL, th.wr(p, ctl, "answer k\n").errno()); + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + var want: [32]u8 = undefined; + try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} del k j\n", .{serial}))); + try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer -\n").reply.status); + try std.testing.expect(p.panes[asked] != null and p.panes[asked].?.prompt == .none); + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer k\n").reply.status); + try std.testing.expect(p.paneBySerial(serial) == null); + } + // Bare Del from anything but a key closes at once and gives the rows up, // as it always has. p.newScratchBelow(top); -- cgit v1.3