From 637ab988f0922173055ea98b1f76916e42f4222a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 05:24:25 -0300 Subject: A prompt a 9P request opens is an ask, answered with answer, never stacked Save through exec on a terminal (or a scratch) opened its path prompt on screen, where no script could see or answer it, and a second Save stacked another. Opened while a 9P request is served, it now logs ask save path; answer on the pane's ctl saves there and closes it, answer - closes it; asked again, the open one stands. A pane closed with it open logs answer -. Co-Authored-By: Claude Opus 5.5 --- src/fs.zig | 3 +++ src/ninep/ctl.zig | 32 ++++++++++++++++++++++++++++++++ src/ninep/events.zig | 2 +- src/pardes.zig | 13 +++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/fs.zig b/src/fs.zig index 2c739115..cb89648c 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -1274,6 +1274,9 @@ pub const Namespace = struct { /// A ctl write's failure is being said: its err record logs it, so /// no msg does (Messages.reportFailure). unlogged: bool = false, + /// A 9P request is being served (Pardes.serveFs): a prompt it opens is + /// a question for the log (`ask`), answered with `answer`. + serving: bool = false, refused: bool = false, failure: [256]u8 = undefined, failure_len: u16 = 0, diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 3282c743..d3b008ff 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -741,6 +741,16 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "answer takes {s} or -", .{choices}) catch std.fmt.bufPrint(&why, "answer takes a choice or -", .{}) catch "answer takes a choice or -"); } return tree.failText(req.tag, E.INVAL, "answer takes a choice or -"); + } else if (std.mem.startsWith(u8, line, "answer ") and p.panes[p.paneBySerial(serial).?].?.prompt == .save) { + // Save's path, asked over 9P (`ask save path`). + if (!apply) continue; + const id = p.paneBySerial(serial).?; + const asked = p.panes[id].?; + const path = std.mem.trim(u8, line["answer ".len..], " "); + var rec: [300]u8 = undefined; + pardes.ctlfs.events.notePath(p, "answer", std.fmt.bufPrint(&rec, "{d} {s}", .{ serial, path[0..@min(path.len, 256)] }) catch ""); + if (!std.mem.eql(u8, path, "-")) exec_line.saveTo(p, id, path); + asked.closePrompt(); } 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..], " "); @@ -1743,3 +1753,25 @@ test "Newcol at the column cap fails the write and says the cap" { try testing.expect(std.mem.indexOf(u8, refused.reply.ename, try std.fmt.bufPrint(&want, "no space for a column: {d} max", .{pardes.MAX_COLS})) != null); try testing.expect(th.logHas(p, "no space for a column")); } + +test "Save on a terminal asked over 9P is an ask in the log, answered with a path, and not asked twice" { + const p = try th.withTerm(testing.allocator); + defer p.deinit(); + const serial = serialOf(p); + const id = p.paneBySerial(serial).?; + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .tagexec), "Save\n").reply.status); + var want: [48]u8 = undefined; + try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} save path\n", .{serial}))); + try testing.expect(p.panes[id].?.prompt == .save); + // Again: the one open stands, one ask in the log. + _ = wr(p, Node.of(serial, .tagexec), "Save\n"); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + try testing.expectEqual(@as(usize, 1), std.mem.count(u8, call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes, "save path\n")); + _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); + // Answered: the path is written to, the prompt goes. + const answered = wr(p, Node.of(serial, .ctl), "answer /tmp/pardes-ask-save.txt\n"); + try testing.expectEqual(Status.ok, answered.reply.status); + try testing.expect(p.panes[id].?.prompt == .none); + try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "answer {d} /tmp/pardes-ask-save.txt\n", .{serial}))); +} diff --git a/src/ninep/events.zig b/src/ninep/events.zig index d2beecd1..105397f3 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -124,7 +124,7 @@ pub fn noteRetire(p: *Pardes, pane: *Pane) void { } // A question still standing ends with it, so a follower of its `ask` // hears the end. - if (pane.prompt == .repl_choice or pane.prompt == .del_side) { + if (pane.prompt == .repl_choice or pane.prompt == .del_side or pane.prompt == .save) { var rec: [32]u8 = undefined; notePath(p, "answer", std.fmt.bufPrint(&rec, "{d} -", .{pane.serial}) catch ""); } diff --git a/src/pardes.zig b/src/pardes.zig index a55a22e0..7e426ba4 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5386,7 +5386,9 @@ pub const Pardes = struct { p.needs_frame = true; p.raw_hover_intent = false; look.cancelLookHover(p); + p.fs.serving = true; const reply = ctlfs.handle(p, req); + p.fs.serving = false; if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NOSPC) or (req.op == .release and req.remove))) ctlfs.events.noteError(p, req, reply); // The request was a whole step of its own, so it settles the way a // step does: the cursor and scroll reconciled, the scripted panes @@ -5632,6 +5634,8 @@ pub const Pardes = struct { const asked = p.panes[id] orelse return null; switch (asked.prompt) { .del_side => return "k j", + // Any path; `-` takes it back. + .save => return "", .repl_choice => { const ask = p.repl_ask orelse return null; var w = std.Io.Writer.fixed(buf); @@ -5990,6 +5994,15 @@ pub const Pardes = struct { p.fs.refused = true; return; } + // Asked for over 9P (Save from an exec on a terminal): a question + // the log says, `ask save path`, answered on the pane's + // ctl with `answer ` or `answer -`; asked again, the one + // already open stands, not a second on top of it. + if (request == .save and p.fs.serving) { + if (pane.prompt == .save) return; + var rec: [32]u8 = undefined; + ctlfs.events.notePath(p, "ask", std.fmt.bufPrint(&rec, "{d} save path", .{pane.serial}) catch ""); + } const marker = switch (request) { .search => |marker| marker, .save => config.save_marker, -- cgit v1.3