summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 07:09:14 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commite3e698fd14c9c7eb6906c95beeeb8e12f08f8b94 (patch)
tree3c0e8cf2c65265dc9220004bfd2d032fd048463c /src
parent1b513045336b3c7381c447ffa8c9167f8b1a5a0a (diff)
downloadpardes-e3e698fd14c9c7eb6906c95beeeb8e12f08f8b94.tar.gz
pardes-e3e698fd14c9c7eb6906c95beeeb8e12f08f8b94.zip
A refusal over unsaved panes logs each as `unsaved <serial> <name>`, and its err never cuts a list
Exit's refusal joined every name into one err, cut at the record's cap in mid-name, so a script could not tell which panes held it up. Each pane is now an `unsaved` record before the err, and the err names the one pane or counts them: `4 unsaved panes: Modified (Exit again to discard)`. Restore, Del and Delcol refuse the same way. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig53
-rw-r--r--src/ninep/events.zig4
2 files changed, 35 insertions, 22 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 13f04859..404e78f4 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -261,10 +261,12 @@ test "Exit over a ctl write or a click names every pane it asks about, a second
defer p.fs.no_prompt = false;
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
try std.testing.expect(!p.quit);
- const said = p.fs.failure[0..p.fs.failure_len];
- try std.testing.expect(std.mem.indexOf(u8, said, first.file.?.path) != null);
- try std.testing.expect(std.mem.indexOf(u8, said, second.file.?.path) != null);
- try std.testing.expect(std.mem.endsWith(u8, said, ": Modified (Exit again to discard)"));
+ // Two: counted, never a list cut short; `unsaved` records name them.
+ try std.testing.expectEqualStrings("2 unsaved panes: Modified (Exit again to discard)", p.fs.failure[0..p.fs.failure_len]);
+ const log = p.fs.log.buf.items;
+ var rec: [64]u8 = undefined;
+ for ([_]*const Pane{ first, second }) |pane|
+ try std.testing.expect(std.mem.indexOf(u8, log, try std.fmt.bufPrint(&rec, "unsaved {d} ", .{pane.serial})) != null);
// Edited since: only that one is named again (acme's rule).
p.fs.failure_len = 0;
panes.File.setContent(p, &second.file.?, try std.testing.allocator.dupe(u8, "y" ** 120));
@@ -474,11 +476,12 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
const Asked = union(enum) { all, pane: usize, column: usize };
fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool {
- var modified = false;
- // Every pane asked about is named in the one refusal a ctl write gets,
- // as acme's rowclean names them all (wind.c:511).
- var all_buf: [1024]u8 = undefined;
- var all = std.Io.Writer.fixed(&all_buf);
+ // Every pane asked about is an `unsaved <serial> <name>` record, as
+ // acme's rowclean names them all (wind.c:511); the refusal a write
+ // gets names the one, or counts them, never a list cut short.
+ var count: usize = 0;
+ var one_buf: [limits.host_path_cap + 24]u8 = undefined;
+ var one: []const u8 = "";
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
switch (which) {
@@ -506,17 +509,22 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool {
var name_buf: [limits.host_path_cap + 24]u8 = undefined;
const name = if (scratch) std.fmt.bufPrint(&name_buf, "{s} (pane {d})", .{ f.path, pane.serial }) catch f.path else f.path;
var buf: [limits.host_path_cap + 72]u8 = undefined;
+ pardes.ctlfs.events.noteLog(c.p, .unsaved, pane);
c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ name, @tagName(asking) }) catch name);
- all.print("{s}{s}", .{ if (modified) ", " else "", name }) catch {};
- modified = true;
+ if (count == 0) one = std.fmt.bufPrint(&one_buf, "{s}", .{name}) catch name;
+ count += 1;
}
- if (modified and (c.p.fs.no_prompt or c.p.fs.capturing)) {
- all.print(": Modified ({s} again to discard)", .{@tagName(asking)}) catch {};
- const said = @import("Messages.zig").clip(all.buffered(), c.p.fs.failure.len);
- @memcpy(c.p.fs.failure[0..said.len], said);
- c.p.fs.failure_len = @intCast(said.len);
+ if (count > 0 and (c.p.fs.no_prompt or c.p.fs.capturing)) {
+ var said_buf: [limits.host_path_cap + 96]u8 = undefined;
+ const said = (if (count == 1)
+ std.fmt.bufPrint(&said_buf, "{s}: Modified ({s} again to discard)", .{ one, @tagName(asking) })
+ else
+ std.fmt.bufPrint(&said_buf, "{d} unsaved panes: Modified ({s} again to discard)", .{ count, @tagName(asking) })) catch "unsaved panes: Modified";
+ const kept = @import("Messages.zig").clip(said, c.p.fs.failure.len);
+ @memcpy(c.p.fs.failure[0..kept.len], kept);
+ c.p.fs.failure_len = @intCast(kept.len);
}
- return modified;
+ return count > 0;
}
/// Quit the editor, refusing once while a pane holds unsaved text: an Exit
@@ -1884,14 +1892,17 @@ test "Exit and Restore name a modified +New scratch as well as files" {
defer p.fs.no_prompt = false;
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
try std.testing.expect(!p.quit);
- const said = p.fs.failure[0..p.fs.failure_len];
- try std.testing.expect(std.mem.indexOf(u8, said, "test.txt") != null);
+ // Each pane's message row names it; the write's refusal counts them.
+ try std.testing.expectEqualStrings("2 unsaved panes: Modified (Exit again to discard)", p.fs.failure[0..p.fs.failure_len]);
+ try std.testing.expect(std.mem.indexOf(u8, file.msg[0..file.msg_len], "test.txt") != null);
+ const scratch_pane = p.panes[p.active].?;
var named: [64]u8 = undefined;
- try std.testing.expect(std.mem.indexOf(u8, said, try std.fmt.bufPrint(&named, "{s} (pane {d})", .{ config.scratch_buffer, p.panes[p.active].?.serial })) != null);
+ try std.testing.expect(std.mem.indexOf(u8, scratch_pane.msg[0..scratch_pane.msg_len], try std.fmt.bufPrint(&named, "{s} (pane {d})", .{ config.scratch_buffer, scratch_pane.serial })) != null);
// Delcol, Restore and Exit name it the same way.
p.fs.failure_len = 0;
try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
- try std.testing.expect(std.mem.indexOf(u8, p.fs.failure[0..p.fs.failure_len], named[0 .. config.scratch_buffer.len + 7]) != null);
+ try std.testing.expect(std.mem.indexOf(u8, scratch_pane.msg[0..scratch_pane.msg_len], "(Delcol again") != null);
+ try std.testing.expect(std.mem.indexOf(u8, scratch_pane.msg[0..scratch_pane.msg_len], named[0 .. config.scratch_buffer.len + 7]) != null);
}
test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and close on the second" {
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 44ad560e..fb0141f4 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -110,7 +110,9 @@ pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply {
// ---- the editor-wide log ----
-pub const LogKind = enum { new, del, rename, save };
+/// `unsaved`: a pane a refusal (Exit, Restore, Del, Delcol) is about,
+/// logged before the write's err so a script reads which, whole.
+pub const LogKind = enum { new, del, rename, save, unsaved };
/// A pane leaving, before its state goes with it; one never announced
/// leaves silently.