summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 12:24:04 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit21d2240b9b86ae5ba0036988b0ed84fbcf18d3af (patch)
treed155d9c7e8f6eb51734cb3dc6544fbf6ceb2e1c5
parentcfcff94ab2b1ff5a0fb61dead60ead09bf892d30 (diff)
downloadpardes-21d2240b9b86ae5ba0036988b0ed84fbcf18d3af.tar.gz
pardes-21d2240b9b86ae5ba0036988b0ed84fbcf18d3af.zip
Restore asks about unsaved text first, as Exit does
Restore replaces every pane with a dump's, so a pane edited since its last save lost that text without a word, where Exit asks. acme's Load only adds a dump's windows and so never asks; Restore now asks what Exit asks (acme's rowclean and winclean, wind.c:511-529): each modified pane says so once, and Restore again with nothing edited since goes ahead. Exit and Restore share the check and its warned-at revision, as acme's winclean clears one dirty flag for any asker. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md3
-rw-r--r--src/builtins.zig56
-rw-r--r--src/panes.zig5
-rw-r--r--test/fs.py7
4 files changed, 51 insertions, 20 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 7d0e797c..2507ac93 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -106,7 +106,8 @@ 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, `<name>: Modified`, and a second `Exit` with nothing edited
-since quits; a scratch under 100 bytes is not asked about); and `Kill`, which
+since quits; a scratch under 100 bytes is not asked about; `Restore`, which
+replaces every pane, asks the same first); and `Kill`, which
does not quit but stops commands, as acme's does: bare, every command pardes
started, and `Kill make ls`, those whose line begins with one of the words. A
command pardes started is a line it typed into a terminal (a word written to
diff --git a/src/builtins.zig b/src/builtins.zig
index 93eae8ec..88c65704 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -226,6 +226,18 @@ test "Exit asks once about unsaved text, and quits when asked again" {
try std.testing.expect(p.quit);
}
+test "Restore asks about unsaved text as Exit does, and restores when asked again" {
+ 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
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon"));
+ try std.testing.expect(p.restore_req == null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified"));
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon"));
+ try std.testing.expectEqualStrings("/tmp/some.dump.zon", p.restore_req.?);
+}
+
test "Kill stops the commands pardes typed, bare all and by name those that match, and never quits" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
@@ -340,26 +352,33 @@ pub const Exec = struct {
// ---- session ----
-/// acme's Exit (editors/acme/exec.c, `exit` and rowclean): quit, unless a
-/// pane holds unsaved text nobody has been told about. Each such pane says
-/// so, `<name>: Modified`, and the Exit goes no further; an Exit after that,
-/// with nothing edited since, quits. A scratch of under 100 bytes is no
-/// loss worth the question, as acme's winclean does not ask for one.
+/// acme's rowclean (editors/acme/wind.c:511-529, winclean), asked before
+/// what would throw unsaved text away: each pane holding some nobody has
+/// been told about says so, `<name>: Modified`, and is then clean for the
+/// asking, as winclean clears `dirty`; so the same word again, with
+/// nothing edited since, goes ahead. A scratch of under 100 bytes is no
+/// loss worth the question, as winclean does not ask for one.
+fn warnModified(c: Ctx) bool {
+ var modified = false;
+ for (c.p.panes, 0..) |slot, id| {
+ const pane = slot orelse continue;
+ const f = pane.file orelse continue;
+ if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue;
+ if (f.output != null and f.content.len < 100) continue;
+ if (pane.discard_warned == f.revision) continue;
+ pane.discard_warned = f.revision;
+ c.p.reportError(id, f.path, error.Modified);
+ modified = true;
+ }
+ return modified;
+}
+
+/// acme's Exit (editors/acme/exec.c:892): quit, once warnModified has
+/// nothing to say.
pub const Exit = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
- var modified = false;
- for (c.p.panes, 0..) |slot, id| {
- const pane = slot orelse continue;
- const f = pane.file orelse continue;
- if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue;
- if (f.output != null and f.content.len < 100) continue;
- if (pane.exit_warned == f.revision) continue;
- pane.exit_warned = f.revision;
- c.p.reportError(id, f.path, error.Modified);
- modified = true;
- }
- if (modified) return;
+ if (warnModified(c)) return;
c.p.quit = true;
c.p.emit(.quit);
}
@@ -412,6 +431,9 @@ pub const Restore = struct {
pub fn run(c: Ctx) void {
const path = c.arg orelse (c.p.last_dump orelse return);
if (path.len > c.p.restore_buf.len) return;
+ // acme's Load adds a dump's windows to the ones there; a Restore
+ // replaces them all, so it asks what Exit asks first.
+ if (warnModified(c)) return;
@memcpy(c.p.restore_buf[0..path.len], path);
c.p.restore_req = c.p.restore_buf[0..path.len];
}
diff --git a/src/panes.zig b/src/panes.zig
index 70b461eb..d4b336d7 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -99,8 +99,9 @@ pub const Pane = struct {
/// middle click, a pty/run): its first word, which Kill matches, and
/// the shell's command count its start (C mark) reaches.
sent_command: ?struct { word: [64]u8 = undefined, len: u8 = 0, seq: u32 = 0 } = null,
- /// The revision an Exit last warned this pane was modified at.
- exit_warned: ?u32 = null,
+ /// The revision an Exit or a Restore last warned this pane was modified
+ /// at (builtins.zig warnModified).
+ discard_warned: ?u32 = null,
/// Native host queues the Linux v9fs launcher in this pane's initial shell.
v9fs_on_spawn: bool = false,
file: ?File.State = null,
diff --git a/test/fs.py b/test/fs.py
index 2199a645..b90983d4 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -601,6 +601,13 @@ def test(binary, quic=False):
assert old.read_fid(held_body) == b'dumped state\n'
assert old.read_fid(held_screen, count=1) == b'{'
old.write('/pane/1/body', b'changed after dump\n', truncate=True)
+ # Unsaved text is asked about once, as Exit asks; the second
+ # Restore goes ahead.
+ try:
+ execute(old, control, 'Restore ' + str(saved))
+ except OSError as refused:
+ assert 'Modified' in str(refused), refused
+ assert old.read('/pane/1/body') == b'changed after dump\n'
try:
execute(old, control, 'Restore ' + str(saved))
except (EOFError, ConnectionResetError, BrokenPipeError):