summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agents/skills/pardes-9p/SKILL.md5
-rw-r--r--docs/fs.md13
-rw-r--r--src/builtins.zig2
-rw-r--r--src/exec.zig5
-rw-r--r--src/ninep/ctl.zig8
-rw-r--r--src/ninep/testing.zig9
-rw-r--r--src/pardes.zig91
7 files changed, 110 insertions, 23 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 845326d2..0b1ef70e 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -142,7 +142,10 @@ but never a 9P write: a script sends code by writing the REPL pane's
`pty/data`, and runs a command from such a file with `Exec <text>` or the
tag. A bound terminal's `ctl` line ends with its id (`python-a`); an event
record written back from such a file's body goes to its REPL, as the click
-would. `Repl -` unbinds; a bare `Repl` says the binding. Multi-line code
+would. `Repl -` unbinds; a bare `Repl` says the binding. With several
+REPLs bound for a language an exec asks which, logged `ask <serial> repl a b`
+(Del's side from the keyboard is `ask <serial> del k j`): answer with
+`echo 'answer a' > $m/pane/<serial>/ctl`, or `answer -` to send nothing. Multi-line code
written to `pty/data` should be a bracketed paste, `\e[200~<code>\e[201~`
then `\r`, and a second `\r` when the code's last line is indented (a `def`
or `for` body): one Enter leaves such a block open. Sent line by line, a blank line ends a Python block, and Python
diff --git a/docs/fs.md b/docs/fs.md
index 2dee3a49..bc48db2f 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -356,7 +356,13 @@ tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`):
whether the pane has the keyboard. It takes the pane's builtins (below),
`get`, which reloads the buffer from the name it
carries (unsaved edits are refused once, `<name>: Modified (get again to
-discard)`, as acme's get asks winclean, exec.c:513), and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an
+discard)`, as acme's get asks winclean, exec.c:513), `answer <choice>`
+for the question the pane asks on its notice band, which the log names as
+`ask <serial> <what> <choices>` -- `ask 4 del k j` for Del's side from the
+keyboard (`k` the pane above takes the rows, `j` the one below), `ask 4
+repl a b` for which bound REPL takes an exec (a REPL's letter) -- `answer -`
+taking it back as Esc does (with no question standing, `answer` is refused),
+and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an
edit of several writes to `addr` and `data` that another client must not
land in the middle of. As in acme the lock binds only the clients that take
it: a `lock` while another open holds it fails at once with `file in use`
@@ -499,8 +505,9 @@ an open renders its frame; stat the entry.
`/log` is one ring (64 KiB) that records whether or not anyone reads it:
`new`, `del`, `rename` (a terminal's too, as its shell changes directory,
since a terminal is named by its directory), `exit <serial> <N>` before
-the `del` of a terminal whose shell exited by itself, and `save <serial>
-<name>`,
+the `del` of a terminal whose shell exited by itself, `ask <serial> <what>
+<choices>` when a pane asks a question (answered by `answer` on its ctl),
+and `save <serial> <name>`,
`dump <path>` when a Dump is written and `restore <path>` in a Restore's
new log after its panes' `new`s, and `msg <serial|-> <text>`
for every line the editor says, repeats included (with `verbose` on, that
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 <serial> <what> <choices>`: 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);