summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 23:26:47 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitfd448801b83bdddf27a1d9af17bf8581356b2b35 (patch)
treeefb2ef616daf0e99ec6a278eabe3e9086ceba837
parent72129eba38522de8f3d0ec6d98b612676d226612 (diff)
downloadpardes-fd448801b83bdddf27a1d9af17bf8581356b2b35.tar.gz
pardes-fd448801b83bdddf27a1d9af17bf8581356b2b35.zip
Del and Delcol refuse unsaved text once, as acme's do
acme's Del asks winclean first and Delcol colclean (exec.c del, delcol): unsaved text is warned about, and the same word again closes. pardes closed at once. Both now refuse once through the one-warning-per-word mechanism Exit uses, from keys, tags, exec and ctl alike. A pane dirty only by a rename, or an empty one that never held a file, has no text to lose and is not asked about, and a scratch or a command's output under 100 bytes is not either, as acme's winclean passes a small unnamed window. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md7
-rw-r--r--docs/tags.md7
-rw-r--r--src/builtins.zig52
-rw-r--r--src/panes.zig2
4 files changed, 58 insertions, 10 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 5093e754..c257a55d 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -119,10 +119,11 @@ every setting is one, the rest act on a pane). `/ctl` takes the session's
builtins, one a line, at whichever pane has the keyboard as each runs --
`Newcol`, `Dump`, `Mount name dial`, `Theme ink`, `Verbose off`; `Exit`,
which quits the editor as acme's does (it refuses once, naming each pane with
-unsaved text, a `+New` scratch with text in it too, in one line,
+unsaved text, a `+New` scratch of 100 bytes or more too, in one line,
`<name>, <name>: Modified (Exit again to discard)`, and a second `Exit`
-with nothing edited since quits, throwing that text away; a command's
-output under 100 bytes is not asked about; `Restore`, which replaces every pane,
+with nothing edited since quits, throwing that text away; a scratch or a
+command's output under 100 bytes is not asked about, as acme's winclean
+asks about no small unnamed window; `Restore`, which replaces every pane,
asks the same first -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in
`DumpDir` (default `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`)
and logs `dump <path>`, and `Restore` with no path takes the last one; a
diff --git a/docs/tags.md b/docs/tags.md
index e6ec0585..f622520f 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -60,7 +60,12 @@ to its `ctl`, they step the body back and forward through its edits, as the
`u` and `U` keys do. They are not in the default tags.
`Del` closes a pane and gives its rows to one neighbor; the rest of the
-column keeps its heights. `Del k` (or `DelAbove`) gives them to the nearest
+column keeps its heights. A pane with unsaved text is refused once, as
+acme's Del warns (exec.c del, wind.c winclean): `<name>: Modified (Del again
+to discard)`, and the same `Del` again, nothing edited since, closes it and
+throws the text away; `Delcol` refuses a column holding such a pane the same
+way (exec.c delcol). Each word warns on its own, from a key, a tag, `exec`
+or a ctl, where the refusal fails the write with EIO. `Del k` (or `DelAbove`) gives them to the nearest
expanded pane above, `Del j` (or `DelBelow`) to the one below, each falling
back to the other side. Bare `Del` from the keyboard (`SPC d`, or Enter on
the word) on a pane with expanded panes both above and below asks on the
diff --git a/src/builtins.zig b/src/builtins.zig
index 384e007f..5b1b4dcb 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -446,6 +446,14 @@ pub const Exec = struct {
/// the person at the keyboard. A scratch of under 100 bytes is no loss
/// worth the question, as winclean does not ask for one.
fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
+ return warnModifiedIn(c, asking, .all);
+}
+
+/// Which panes a refusal asks about: every one (Exit, Restore), one (Del),
+/// or a column's (Delcol).
+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).
@@ -453,12 +461,23 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
var all = std.Io.Writer.fixed(&all_buf);
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
+ switch (which) {
+ .all => {},
+ .pane => |only| if (id != only) continue,
+ .column => |col| if ((layout.findPane(c.p, id) orelse continue).col != col) continue,
+ }
const f = pane.file orelse continue;
- // A +New scratch saves nowhere, but what is typed in it is as lost.
+ // A +New scratch saves nowhere, but what is typed in it is as lost;
+ // under 100 bytes it is not asked about, as acme's winclean does not
+ // whine about a small unnamed window, nor about a command's output.
const scratch = if (f.output) |o| o.from == .cmd and o.from.cmd == .New else false;
if (f.revision == f.saved_revision or !(panes.Output.fileTraits(f.output).saves or scratch)) continue;
- if (f.output != null and !scratch and f.content.len < 100) continue;
- if (scratch and f.content.len == 0) continue;
+ if (f.output != null and f.content.len < 100) continue;
+ // Dirty by a rename alone is no text to lose, as get says (ctl.zig).
+ // Nor is an empty buffer that never held a file.
+ if (f.saved_hash) |saved| {
+ if (std.hash.Wyhash.hash(0, f.content) == saved) continue;
+ } else if (f.content.len == 0) continue;
if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == asking) continue;
pane.discard_warned = .{ .revision = f.revision, .by = asking };
var buf: [limits.host_path_cap + 48]u8 = undefined;
@@ -847,6 +866,9 @@ pub const Del = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
const side = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
+ // acme's Del asks winclean first (exec.c del): unsaved text is warned
+ // about once, and the same Del again, nothing edited since, closes.
+ if (warnModifiedIn(c, .Del, .{ .pane = c.id })) return;
const at = layout.findPane(c.p, c.id);
const above = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .above) else null;
const below = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .below) else null;
@@ -956,6 +978,9 @@ pub const Delcol = struct {
// The column of the tag it came from, else the pane's: a pane ctl's
// Delcol closes that pane's column wherever the keyboard is.
const col = c.p.exec_column orelse (layout.findPane(c.p, c.id) orelse return).col;
+ // acme's Delcol refuses a column with unsaved text (exec.c delcol,
+ // cols.c colclean), once: the next Delcol closes it.
+ if (warnModifiedIn(c, .Delcol, .{ .column = col })) return;
c.p.removeColumn(col) catch |err| c.p.reportError(c.id, "close column", err);
}
};
@@ -1744,8 +1769,8 @@ test "Exit and Restore name a modified +New scratch as well as files" {
file.file.?.saved_revision = file.file.?.revision -% 1; // modified
p.newScratchBelow(p.active);
const scratch = &p.panes[p.active].?.file.?;
- scratch.content = try p.gpa.realloc(scratch.content, 6);
- @memcpy(scratch.content, "notes\n");
+ scratch.content = try p.gpa.realloc(scratch.content, 120);
+ @memset(scratch.content, 'n'); // past acme's 100 bytes, where it whines
scratch.revision +%= 1; // typed into, never saved
p.fs.no_prompt = true;
defer p.fs.no_prompt = false;
@@ -1755,3 +1780,20 @@ test "Exit and Restore name a modified +New scratch as well as files" {
try std.testing.expect(std.mem.indexOf(u8, said, "test.txt") != null);
try std.testing.expect(std.mem.indexOf(u8, said, config.scratch_buffer) != null);
}
+
+test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and close on the second" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("saved\n");
+ pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ const serial = pane.serial;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
+ try std.testing.expect(p.paneBySerial(serial) != null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Delcol again to discard)"));
+ // Each word is warned on its own: Del asks too, then closes.
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) != null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Del again to discard)"));
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) == null);
+}
diff --git a/src/panes.zig b/src/panes.zig
index 8ca2bb4d..c33fd577 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -25,7 +25,7 @@ pub const EditText = struct { text: []u8, row0: i32 };
pub const Pane = struct {
/// The words that throw unsaved text away (builtins.zig warnModified).
- pub const Discarding = enum { Exit, Restore, get };
+ pub const Discarding = enum { Exit, Restore, get, Del, Delcol };
/// One mouse sweep, per button: the press (c0, r0) and where it is now
/// (c1, r1), a stream between them as acme's (edit.sweepCols). c/r are