diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 07:05:46 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 1b513045336b3c7381c447ffa8c9167f8b1a5a0a (patch) | |
| tree | bf9377e7178f367c3ad0b35446f4aa9eaf9600d2 | |
| parent | 96383331f09e8af3fa24f767b3c56863309723b8 (diff) | |
| download | pardes-1b513045336b3c7381c447ffa8c9167f8b1a5a0a.tar.gz pardes-1b513045336b3c7381c447ffa8c9167f8b1a5a0a.zip | |
A builtin's Verbose announcement is logged only when it succeeds
A failure said while it ran (Exit's refusal, a Del refused) or later by
the host (a Save's write, a Dump's) left `msg N Save` in the log as if it
had worked. The announcement's record is remembered and taken back out
on either, unless a follower read it already. A refused pane/new logs its
err alone, and a failed Dump says its reason in words (`file not found`,
not `FileNotFound`) so its msg goes and the err is the one record.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/fs.md | 11 | ||||
| -rw-r--r-- | src/Messages.zig | 4 | ||||
| -rw-r--r-- | src/exec.zig | 10 | ||||
| -rw-r--r-- | src/fs.zig | 10 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 32 | ||||
| -rw-r--r-- | src/ninep/events.zig | 38 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 7 | ||||
| -rw-r--r-- | src/pardes.zig | 5 |
8 files changed, 110 insertions, 7 deletions
@@ -720,10 +720,13 @@ mapping the serial it had to the one it has now, and `restoredcol <old> for every line the editor says (with `verbose` on, that includes each builtin announcing itself as it runs, on purpose: the log says which ran -- unless the builtin then says something of its own that starts with its -name, `Kill: nothing running`, which takes the announcement's place; a -builtin that fails its write (`ctl`, `exec`, `/tagexec` or a column's -`exec`) is logged by that write's `err` alone, no announcement and no `msg`, so the same failure again is the same record -again; a line said again word for word is counted, `msg 3 Undo: nothing to +name, `Kill: nothing running`, which takes the announcement's place; an +announcement is logged only for a builtin that succeeds: one that fails, +as it runs or when the host could not do its work (a Save's write, a +Dump's), leaves none, from a click at the keyboard too. A builtin that +fails its write (`ctl`, `exec`, `/tagexec` or a column's `exec`, and an +open of `pane/new`) is logged by that write's `err` alone, no announcement +and no `msg`, so the same failure again is the same record again; a line said again word for word is counted, `msg 3 Undo: nothing to undo (x40)`, as `err` is (below); a text past 256 bytes is cut there, between words, and ends in an ellipsis, `…`, as an `err`'s reason is past 200; its serial is the pane it ran at, `-` when the keyboard was on a diff --git a/src/Messages.zig b/src/Messages.zig index eb2fa5e2..c0838fb2 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -485,6 +485,10 @@ pub fn clip(text: []const u8, max: usize) []const u8 { /// reportError with the words already chosen. pub fn reportFailure(p: *Pardes, id: usize, text: []const u8) void { + p.fs.failures +%= 1; + // A failure said later, when the host could not do the work (a Save's + // write): that builtin's announcement goes from the log too. + ctlfs.events.dropFailedAnnouncement(p, text, false); // A builtin a ctl write runs: its first error is also the write's, cut // between words. const failing_write = p.fs.no_prompt or p.fs.capturing; diff --git a/src/exec.zig b/src/exec.zig index c2d1be21..aee2e1e5 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -789,12 +789,22 @@ pub fn runBuiltinFrom(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ? // A builtin says its own name before it runs, so a tagline click is // legible even when what it did is invisible. Whatever the builtin // then says for itself replaces the announcement. + // Its log record stands only if it does not fail (events.dropFailedAnnouncement). + var announced: ?u64 = null; if (p.announce and p.settings.verbose and builtins.registry.announces(b)) { p.setMessage(id, @tagName(b)); pane.msg_announcement = true; + announced = pardes.ctlfs.events.newestSaying(p, @tagName(b)); + p.fs.announced_seq = announced; + const word = @tagName(b)[0..@min(@tagName(b).len, p.fs.announced_word.len)]; + @memcpy(p.fs.announced_word[0..word.len], word); + p.fs.announced_word_len = @intCast(word.len); } + const failures = p.fs.failures; const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg, .from_body = from_body }; builtins.registry.dispatch(b, c); + if (announced != null and p.fs.failures != failures and p.fs.announced_seq == announced) + pardes.ctlfs.events.dropFailedAnnouncement(p, "", true); } pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]const u8) void { @@ -1298,6 +1298,16 @@ pub const Namespace = struct { header_held: bool = false, late_failure: [256]u8 = undefined, late_failure_len: u16 = 0, + /// The log record of the last Verbose announcement (`msg <serial> + /// <Word>`) and its word: a failure of that builtin, now or when its + /// host work fails (a Save, a Dump), takes it back out of the log, so + /// an announcement stands only for a builtin that did what it said. + announced_seq: ?u64 = null, + /// Every failure said (Messages.reportFailure), counted: a builtin + /// that said one while it ran failed. + failures: u32 = 0, + announced_word: [32]u8 = undefined, + announced_word_len: u8 = 0, /// A refusal that quotes the message it refuses, as Plan 9's cmderror /// does (kernel/misc/parse.c:82); answered at once (src/9p_io.zig). ename: [320]u8 = undefined, diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index de63c6aa..b6fefa01 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1486,6 +1486,36 @@ test "look and exec read back what the last command touched, a ctl write's too" try testing.expectEqualStrings("", rd(p, @intFromEnum(tree.TopFile.look), 0, 64).bytes); } +test "a builtin's Verbose announcement is logged only when it succeeds, its failure said now or by the host later" { + const gpa = testing.allocator; + const p = try withFile(gpa, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + var want: [64]u8 = undefined; + const said_exit = try std.fmt.bufPrint(&want, "msg {d} Exit\n", .{serial}); + // Refused as it runs, through a click: the write fails, and no Exit. + const f = &p.panes[0].?.file.?; + f.saved_revision = f.revision -% 1; + f.saved_hash = 0; + try testing.expect(wr(p, Node.of(serial, .exec), "Exit\n").errno() != 0); + try testing.expect(!th.logHas(p, said_exit)); + // Refused at the keyboard (no write to fail), edited since: the same. + f.revision +%= 1; + _ = p.executeBuiltinLine(p.active, "Exit"); + try testing.expect(!th.logHas(p, said_exit)); + try testing.expect(!p.quit); + // A Save the host could not do, told a step later: the Save goes too. + var save_buf: [64]u8 = undefined; + const said_save = try std.fmt.bufPrint(&save_buf, "msg {d} Save\n", .{serial}); + _ = wr(p, Node.of(serial, .exec), "Save\n"); + p.saveFailed(@intCast(p.paneBySerial(serial).?), "/test.txt", error.AccessDenied); + try testing.expect(!th.logHas(p, said_save)); + // One that does what it says keeps its announcement. + _ = wr(p, Node.of(serial, .exec), "Undo\n"); + var undo_buf: [64]u8 = undefined; + try testing.expect(th.logHas(p, try std.fmt.bufPrint(&undo_buf, "msg {d} Undo", .{serial}))); +} + test "an error's words give the errno a mount reads: EINVAL for what is malformed, EIO for what failed" { // 9ns (cloud9's 9ns/src/nine.zig) maps an error to an errno by its // words, and EIO when none of them match: these words must not. @@ -1574,7 +1604,7 @@ test "a Restore of a file that is no dump fails the write before any warning, an p.fs.late_failure_len = 0; p.dumpFailed("/nowhere/pardes.dump.zon", error.FileNotFound); - try testing.expectEqualStrings("Dump /nowhere/pardes.dump.zon: FileNotFound", p.fs.late_failure[0..p.fs.late_failure_len]); + try testing.expectEqualStrings("Dump /nowhere/pardes.dump.zon: file not found", p.fs.late_failure[0..p.fs.late_failure_len]); } test "a setting this frontend cannot show says GUI-only, and DumpDir reads back the directory in effect" { diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 2b7c1c1d..44ad560e 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -232,6 +232,44 @@ pub fn dropAnnouncement(p: *Pardes, serial: u32, text: []const u8) void { p.fs.log.buf.shrinkRetainingCapacity(last.at); } +/// The sequence number of the newest log record when it is a msg that +/// ends in `word` alone: not one counted (`(x2)`) over an earlier say. +pub fn newestSaying(p: *Pardes, word: []const u8) ?u64 { + const last = newest(p) orelse return null; + if (!std.mem.startsWith(u8, last.text, "msg ") or !std.mem.endsWith(u8, last.text, word)) return null; + if (last.text[last.text.len - word.len - 1] != ' ') return null; + return last.seq; +} + +/// Takes record `seq` back out of the log, unless a follower has read it. +pub fn dropRecord(p: *Pardes, seq: u64) void { + if (followerRead(p, seq)) return; + const q = &p.fs.log; + var at = q.head; + var s = q.dropped; + while (at + 4 <= q.buf.items.len) : (s += 1) { + const size = 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little); + if (s == seq) { + const items = q.buf.items; + std.mem.copyForwards(u8, items[at..], items[at + size ..]); + q.buf.shrinkRetainingCapacity(items.len - size); + return; + } + at += size; + } +} + +/// A builtin's failure: its Verbose announcement goes from the log, when +/// `text` is that builtin's (it starts with its word) or `any`, as for a +/// failure said while the builtin itself runs. +pub fn dropFailedAnnouncement(p: *Pardes, text: []const u8, any: bool) void { + const seq = p.fs.announced_seq orelse return; + const word = p.fs.announced_word[0..p.fs.announced_word_len]; + if (!any and !(std.mem.startsWith(u8, text, word) and text.len > word.len and (text[word.len] == ' ' or text[word.len] == ':'))) return; + p.fs.announced_seq = null; + dropRecord(p, seq); +} + /// Drops the newest record when it is a msg saying `text` (of any pane) /// that no follower has read: a failure its write's err record will say. pub fn dropMessage(p: *Pardes, text: []const u8) void { diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 69b89f53..ed0e0352 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -717,7 +717,12 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { .new => { const slot = p.freeSlot() orelse return failText(req.tag, E.NOSPC, std.fmt.bufPrint(&p.fs.ename, "pane/new: " ++ pardes.Messages.no_pane_slots, .{pardes.MAX_PANES}) catch "no space for a pane"); p.fs.no_pane_slot = false; + // Refused, the open's err is the one record of it (the + // one rule): no msg besides. + p.fs.capturing = true; p.newScratchFor(p.active, .fs); + p.fs.capturing = false; + p.fs.failure_len = 0; const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE); // No room in its column: it closes at the step's end. if (p.unplaced.isSet(slot)) return failText(req.tag, E.NOSPC, p.fs.ename[0..p.fs.no_pane_slot_len]); @@ -1448,6 +1453,8 @@ test "at the pane cap, pane/new, look and New each say so, and look reads back e try testing.expectEqual(E.NOSPC, refused.errno()); try testing.expect(std.mem.endsWith(u8, refused.reply.ename, said)); try testing.expect(th.logHas(p, said)); + // Its err alone says it: no msg besides (the one rule). + try testing.expect(!th.logHas(p, "New: no space")); // A look that would open a pane fails its write, and says the same. var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); diff --git a/src/pardes.zig b/src/pardes.zig index 98b8819c..739d5bf2 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5219,8 +5219,9 @@ pub const Pardes = struct { var what: [limits.host_path_cap + 8]u8 = undefined; const operation = std.fmt.bufPrint(&what, "Dump {s}", .{path}) catch "Dump"; p.reportError(p.active, operation, err); - var buf: [256]u8 = undefined; - const said = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch "Dump failed"; + // In the words the message row has, so the write's err is the one + // record of it (its msg goes: 9p_io, dropMessage). + const said = if (p.panes[p.active]) |pane| pane.msg[0..pane.msg_len] else "Dump failed"; const kept = @import("Messages.zig").clip(said, p.fs.late_failure.len); @memcpy(p.fs.late_failure[0..kept.len], kept); p.fs.late_failure_len = @intCast(kept.len); |
