summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 02:20:12 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commita4a8b1874d477498fb427f8e2f63382c99f618a0 (patch)
tree40f7117b6ea5848e0495a2cf075898e049bf0871
parent964709e115126aafa6fd5d8c984d511cf68cecaf (diff)
downloadpardes-a4a8b1874d477498fb427f8e2f63382c99f618a0.tar.gz
pardes-a4a8b1874d477498fb427f8e2f63382c99f618a0.zip
A msg said again word for word is that line counted in the log
1000 Undos with nothing left made 2000 log lines: the Verbose announcement and the answer each time. A msg repeating the newest record is now that record counted, (xN), as err's already were (shared as pushCounting), unless a follower has read it; and a builtin's own words that start with its name take the place of its announcement just before them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md9
-rw-r--r--src/ninep/ctl.zig7
-rw-r--r--src/ninep/events.zig71
3 files changed, 62 insertions, 25 deletions
diff --git a/docs/fs.md b/docs/fs.md
index d2ab3919..d8080e1c 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -617,9 +617,12 @@ and `save <serial> <name>`,
new log after its panes' `new`s, then `restored <old> <new>` for each pane,
mapping the serial it had to the one it has now, and `restoredcol <old>
<new>` for each column, and `msg <serial|-> <text>`
-for every line the editor says, repeats included (with `verbose` on, that
-includes each builtin announcing itself as it runs, on purpose: the log says
-which ran; its serial is the pane it ran at, `-` when the keyboard was on a
+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 line
+said again word for word before any follower read it is that line counted,
+`msg 3 Undo: nothing to undo (x40)`, as `err` is; its serial is the pane it ran at, `-` when the keyboard was on a
column or workspace tag, or the line came to the root's ctl, `/tagexec` or
a column's ctl or exec), and `err <serial|->
<file>: <why>` for every write or truncation the tree refused or that
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index d6922390..8a35c381 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -730,6 +730,9 @@ test "Undo and Redo on a pane's ctl step its body through its edits" {
_ = wr(p, Node.of(serial, .ctl), "Undo");
try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status);
try testing.expect(th.logHas(p, "Undo: nothing to undo"));
+ // Said again word for word, it is that line counted, as err's are.
+ for (0..3) |_| _ = wr(p, Node.of(serial, .ctl), "Undo");
+ try testing.expect(th.logHas(p, "Undo: nothing to undo (x4)\n"));
}
test "focus reads the pane with the keyboard, and a serial written to it moves the keyboard there" {
@@ -971,8 +974,8 @@ test "a message from a root ctl write is the session's in the log" {
const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 8192 }).bytes.len;
_ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" });
try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Kill\n").reply.status);
- // The builtin's name, then what it said: both the session's.
- try testing.expectEqualStrings("msg - Kill\n", call(p, .{ .tag = 4, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes);
+ // What it said, the session's; its own words name it, so its Verbose
+ // announcement gives way to them.
try testing.expectEqualStrings("msg - Kill: nothing running\n", call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes);
}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 7402a763..2112110a 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -145,7 +145,20 @@ pub fn noteLog(p: *Pardes, kind: LogKind, pane: *Pane) void {
/// Records `msg <serial> <text>` for what the editor said, `-` for no pane.
pub fn noteMessage(p: *Pardes, serial: u32, text: []const u8) void {
var buf: [pardes.Messages.LoggedMessage.cap + 32]u8 = undefined;
- pushLog(p, (if (serial == 0)
+ // A builtin's own word for what it did stands in for its Verbose
+ // announcement just before (`Undo` then `Undo: nothing to undo`), so
+ // that 1000 Undos with nothing left are one line, counted, as err's are.
+ if (newest(p)) |last| if (!followerRead(p, last.seq) and std.mem.startsWith(u8, last.text, "msg ")) {
+ const rest = last.text["msg ".len..];
+ const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse rest.len;
+ const word = rest[@min(sp + 1, rest.len)..];
+ var who: [16]u8 = undefined;
+ const mine = if (serial == 0) "-" else std.fmt.bufPrint(&who, "{d}", .{serial}) catch "";
+ if (std.mem.eql(u8, rest[0..sp], mine) and word.len > 0 and std.mem.indexOfScalar(u8, word, ' ') == null and
+ text.len > word.len and std.mem.startsWith(u8, text, word) and text[word.len] == ':')
+ p.fs.log.buf.shrinkRetainingCapacity(last.at);
+ };
+ pushCounting(p, (if (serial == 0)
std.fmt.bufPrint(&buf, "msg - {s}\n", .{text})
else
std.fmt.bufPrint(&buf, "msg {d} {s}\n", .{ serial, text })) catch return);
@@ -196,10 +209,35 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') {
c.* = ' ';
};
+ pushCounting(p, record);
+}
+
+/// Pushes `record`, or, when it repeats the newest record word for word,
+/// counts that one instead: `<record> (x2)`.
+fn pushCounting(p: *Pardes, record: []u8) void {
// A client retrying a write that fails the same way would fill the ring
// with one line, so a repeat of the newest record is that record counted,
// `(x3)`, as +Messages counts its repeats; unless a follower has read it
// already and so waits on the repeat as a line of its own.
+ const last = newest(p) orelse return pushLog(p, record);
+ var said = last.text;
+ var times: u32 = 1;
+ if (std.mem.endsWith(u8, said, ")")) if (std.mem.lastIndexOf(u8, said, " (x")) |x| {
+ if (std.fmt.parseInt(u32, said[x + 3 .. said.len - 1], 10)) |n| {
+ times = n;
+ said = said[0..x];
+ } else |_| {}
+ };
+ if (!std.mem.eql(u8, said, record[0 .. record.len - 1]) or followerRead(p, last.seq)) return pushLog(p, record);
+ var counted: [pardes.Messages.LoggedMessage.cap + 64]u8 = undefined;
+ const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return;
+ p.fs.log.buf.shrinkRetainingCapacity(last.at);
+ pushLog(p, again);
+}
+
+/// The newest record in the ring: where it starts, its sequence number and
+/// its text without the newline.
+fn newest(p: *Pardes) ?struct { at: usize, seq: u64, text: []const u8 } {
const q = &p.fs.log;
var at = q.head;
var last: ?usize = null;
@@ -208,26 +246,18 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
last = at;
at += 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little);
}
- const last_at = last orelse return pushLog(p, record);
- const last_seq = seq - 1;
- var said = q.buf.items[last_at + 4 ..];
- said = said[0 .. said.len - 1];
- var times: u32 = 1;
- if (std.mem.endsWith(u8, said, ")")) if (std.mem.lastIndexOf(u8, said, " (x")) |x| {
- if (std.fmt.parseInt(u32, said[x + 3 .. said.len - 1], 10)) |n| {
- times = n;
- said = said[0..x];
- } else |_| {}
- };
- if (!std.mem.eql(u8, said, record[0 .. record.len - 1])) return pushLog(p, record);
+ const last_at = last orelse return null;
+ const text = q.buf.items[last_at + 4 ..];
+ return .{ .at = last_at, .seq = seq - 1, .text = text[0 .. text.len - 1] };
+}
+
+/// Whether a follower has read the record `seq`, or is part way through it.
+fn followerRead(p: *Pardes, seq: u64) bool {
for (p.fs.opens) |o| if (o.node != 0 and o.what == .log) {
const l = o.what.log;
- if (l.follow and (l.next > last_seq or (l.next == last_seq and l.part > 0))) return pushLog(p, record);
+ if (l.follow and (l.next > seq or (l.next == seq and l.part > 0))) return true;
};
- var counted: [280]u8 = undefined;
- const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return;
- q.buf.shrinkRetainingCapacity(last_at);
- pushLog(p, again);
+ return false;
}
/// The log is one ring that records whether or not anyone reads it. A record
@@ -851,10 +881,10 @@ test "the log records whether or not anyone reads, and an open that follows wait
try testing.expectEqual(Status.again, rdf.next(p, log, fh, frozen).reply.status);
// A shell's `read` asks for a few bytes at a time: the record comes in
- // pieces rather than failing. Every repeat is its own line.
+ // pieces rather than failing. A repeat the follower waits on, having
+ // read the first, is its own line.
const said_by = p.panes[0].?.serial;
p.setMessage(0, "same failure");
- p.setMessage(0, "same failure");
var pieced: [64]u8 = undefined;
var got: usize = 0;
while (got == 0 or pieced[got - 1] != '\n') {
@@ -863,6 +893,7 @@ test "the log records whether or not anyone reads, and an open that follows wait
got += piece.len;
}
try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), pieced[0..got]);
+ p.setMessage(0, "same failure");
try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), rdf.next(p, log, fh, frozen).bytes);
// A follower the ring outran hears how much it missed, then carries on.