diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 07:09:14 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | e3e698fd14c9c7eb6906c95beeeb8e12f08f8b94 (patch) | |
| tree | 3c0e8cf2c65265dc9220004bfd2d032fd048463c /src | |
| parent | 1b513045336b3c7381c447ffa8c9167f8b1a5a0a (diff) | |
| download | pardes-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.zig | 53 | ||||
| -rw-r--r-- | src/ninep/events.zig | 4 |
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. |
