diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/fs-help.txt | 9 | ||||
| -rw-r--r-- | src/fs.zig | 10 | ||||
| -rw-r--r-- | src/memory.zig | 2 | ||||
| -rw-r--r-- | src/ninep/events.zig | 190 | ||||
| -rw-r--r-- | src/ninep/screen.zig | 8 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 46 | ||||
| -rw-r--r-- | src/panes.zig | 2 | ||||
| -rw-r--r-- | src/pardes.zig | 19 |
8 files changed, 233 insertions, 53 deletions
diff --git a/src/fs-help.txt b/src/fs-help.txt index 7f55b3af..dca2c14e 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -5,7 +5,8 @@ index one line per pane: serial, kind (text|term|pdf|image), dirty flag, n status pid, version and pane count look write a line: a right click on it at the active pane; read: the serials it touched exec write a line: a middle click, an editor command word or a shell line; read the same -log one line per editor event (new/del/rename/save <serial> <name>); reads wait +log recent events, one a line: new/del/rename/save <serial> <name>, and + msg <serial|-> <text> for what the editor said; write follow to wait screen rendered screen as JSON, frozen from open to close listeners the session's dial addresses pane/new open it to make a pane; the read answers that pane's serial @@ -26,9 +27,10 @@ Below, $m is the mount point (PARDES_MOUNT in a Tty9p shell; 9ns and 9p work too echo '#0,#5' > $m/pane/$n/addr; printf NEW >> $m/pane/$n/data replace bytes 0..5 cp $m/pane/$n/addr $m/pane/$n/dot; cat $m/pane/$n/sel select the range, read it cat $m/pane/$n/dirty; echo 0 > $m/pane/$n/dirty is it modified? say it is not - cat $m/log block until a pane is made, renamed, saved or closed + exec 3<>$m/log; echo follow >&3; cat <&3 what happened lately, then wait for more cat $m/pane/$n/event hold it open to own that pane's clicks echo exec > $m/pane/$n/pty/ctl restart a shell; also winsize C R, sig INT + printf 'ls\r' > $m/pane/$n/pty/data type into a terminal; pty/status: cols rows busy Pitfalls, one each: Each open of pane/new makes another pane; two reads of one fid name the same one. @@ -39,6 +41,5 @@ Pitfalls, one each: Writing body appends; open it with truncation to replace; writes are whole UTF-8 sequences. Truncating tag clears the part you may edit; truncating dot or addr empties it. A terminal's body is a history snapshot frozen per open; pty/data is the live stream. - screen is frozen per open handle as well; reopen it for a fresh frame. - A failing command is reported in the editor, not as a write error; a bad line fails the write. + A failing command is reported in the editor and in log, not as a write error; a bad line fails the write. pane/<n>/ctl reads acme's window status line and takes one verb, get, which reloads from disk. @@ -308,8 +308,10 @@ test "virtual writes enforce permissions even when contents are empty" { const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); for ([_][]const u8{ "", "bytes" }) |bytes| { - for ([_][]const u8{ "/virtual/index", "/virtual/screen", "/virtual/README", "/n/self/log" }) |path| + for ([_][]const u8{ "/virtual/index", "/virtual/screen", "/virtual/README" }) |path| try std.testing.expectError(error.ReadOnlyFilesystem, write(p, path, bytes)); + // /log takes one word, `follow`, and only on the open it changes. + if (bytes.len > 0) if (write(p, "/n/self/log", bytes)) |_| return error.TestUnexpectedResult else |_| {}; try std.testing.expectError(error.IsDirectory, write(p, "/virtual/pane", bytes)); try std.testing.expectError(error.FileNotFound, write(p, "/virtual/missing", bytes)); try std.testing.expectError(error.FileNotFound, write(p, "/virtual/cons", bytes)); @@ -1240,9 +1242,9 @@ pub const Namespace = struct { panes: [MAX_PANES]tree.pane.State = @splat(.{}), listeners: u16 = 0, origin: u8 = 'K', - /// The editor-wide event stream, recorded only while /log is held open. - log: tree.events.Queue = .{}, - log_readers: u16 = 0, + /// The editor-wide event ring: panes made, renamed, saved and closed, and + /// what the editor said. Recorded whether or not anyone reads /log. + log: tree.events.Queue = .{ .cap = limits.log_bytes }, /// What the last look or exec created or touched, answered by reading /// either of those files. results: [MAX_PANES]u32 = undefined, diff --git a/src/memory.zig b/src/memory.zig index b8a64a1a..7b6ee2c0 100644 --- a/src/memory.zig +++ b/src/memory.zig @@ -17,6 +17,8 @@ pub const limits = struct { pub const wrap_rows = if (board) 128 else 256; pub const undo_max = if (board) 16 else 256; pub const message_log = if (board) 16 else 128; + /// /log's ring, which records whether or not anyone reads it. + pub const log_bytes: usize = if (board) 4 * KiB else 64 * KiB; pub const max_tag_tail: usize = if (board) 512 else 4096; pub const host_path_cap: usize = if (board) 0 else 4095; /// -Dembed-sources: the editor serves its own sources under /src. diff --git a/src/ninep/events.zig b/src/ninep/events.zig index d94fb54d..472a114f 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -25,6 +25,10 @@ pub fn now() u32 { pub const Queue = struct { buf: std.ArrayList(u8) = .empty, head: usize = 0, + cap: usize = queue_cap, + /// Records pushed out of the front to make room: the sequence number of + /// the oldest one still held. + dropped: u64 = 0, pub fn deinit(q: *Queue, gpa: std.mem.Allocator) void { q.buf.deinit(gpa); @@ -33,9 +37,10 @@ pub const Queue = struct { pub fn push(q: *Queue, gpa: std.mem.Allocator, record: []const u8) void { if (record.len > std.math.maxInt(u32)) return; - while (q.buf.items.len - q.head + record.len + 4 > queue_cap) { + while (q.buf.items.len - q.head + record.len + 4 > q.cap) { if (q.peek() == null) return; q.pop(); + q.dropped += 1; } q.compact(); var head: [4]u8 = undefined; @@ -136,15 +141,114 @@ pub fn announce(p: *Pardes) void { } } -/// Records `<kind> <serial> <name>` while a reader holds /log open. +/// Records `<kind> <serial> <name>`. pub fn noteLog(p: *Pardes, kind: LogKind, pane: *Pane) void { - if (p.fs.log_readers == 0) return; var buf: [4096 + 64]u8 = undefined; const name = pane_files.nameOf(pane); - const record = std.fmt.bufPrint(&buf, "{s} {d} {s}\n", .{ @tagName(kind), pane.serial, name[0..@min(name.len, 4096)] }) catch return; + pushLog(p, std.fmt.bufPrint(&buf, "{s} {d} {s}\n", .{ @tagName(kind), pane.serial, name[0..@min(name.len, 4096)] }) catch return); +} + +/// 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.LoggedMessage.cap + 32]u8 = undefined; + pushLog(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); +} + +/// The log is one ring that records whether or not anyone reads it. A record +/// is one line: a newline in a message or a name would read as two records. +fn pushLog(p: *Pardes, record: []u8) void { + for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') { + c.* = ' '; + }; p.fs.log.push(p.gpa, record); } +/// An open freezes the ring's text, so `cat log` answers what happened lately +/// and ends; `next` is where a follower picks up after it. +pub fn openLog(p: *Pardes, req: Req) Reply { + for (&p.fs.snapshots, 0..) |*slot, i| { + if (slot.node != 0) continue; + var text: std.ArrayList(u8) = .empty; + var at = p.fs.log.head; + const items = p.fs.log.buf.items; + var n: u64 = 0; + while (at + 4 <= items.len) : (n += 1) { + const len = std.mem.readInt(u32, items[at..][0..4], .little); + text.appendSlice(p.gpa, items[at + 4 ..][0..len]) catch { + text.deinit(p.gpa); + return Reply.fail(req.tag, E.NOMEM); + }; + at += 4 + len; + } + const bytes = text.toOwnedSlice(p.gpa) catch return Reply.fail(req.tag, E.NOMEM); + slot.* = .{ .node = req.node, .bytes = bytes, .next = p.fs.log.dropped + n }; + return .{ .tag = req.tag, .handle = @intCast(i + 1) }; + } + return Reply.fail(req.tag, E.NFILE); +} + +/// Past the frozen text, a following open reads newer records one per read, +/// parked until one arrives; one that fell behind the ring hears `lost N`. +pub fn readLog(p: *Pardes, req: Req) Reply { + if (req.handle == 0 or req.handle > p.fs.snapshots.len) return Reply.fail(req.tag, E.INVAL); + const slot = &p.fs.snapshots[req.handle - 1]; + if (slot.node != req.node) return Reply.fail(req.tag, E.INVAL); + const frozen = slot.bytes orelse &.{}; + const out = p.fs.stage(p.gpa); + if (!slot.follow or req.off < frozen.len) { + const off = @min(req.off, frozen.len); + const len = @min(frozen.len - off, req.size); + out.appendSlice(p.gpa, frozen[off..][0..len]) catch return Reply.fail(req.tag, E.NOMEM); + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(len) } }; + } + const q = &p.fs.log; + if (slot.next < q.dropped) { + out.print(p.gpa, "lost {d}\n", .{q.dropped - slot.next}) catch return Reply.fail(req.tag, E.NOMEM); + slot.next = q.dropped; + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } }; + } + // ponytail: walks the ring from its oldest record; it holds at most log_bytes. + var at = q.head; + var seq = q.dropped; + while (at + 4 <= q.buf.items.len) : (seq += 1) { + const len = std.mem.readInt(u32, q.buf.items[at..][0..4], .little); + if (seq == slot.next) { + if (req.size < len) return Reply.fail(req.tag, E.INVAL); + out.appendSlice(p.gpa, q.buf.items[at + 4 ..][0..len]) catch return Reply.fail(req.tag, E.NOMEM); + slot.next += 1; + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(len) } }; + } + at += 4 + len; + } + return .{ .tag = req.tag, .status = .again }; +} + +/// `follow`, written to an open of the log, makes that open wait for newer +/// records instead of ending; closing it is the way back. +pub fn writeLog(p: *Pardes, req: Req) Reply { + if (req.handle == 0 or req.handle > p.fs.snapshots.len) return Reply.fail(req.tag, E.INVAL); + const slot = &p.fs.snapshots[req.handle - 1]; + if (slot.node != req.node) return Reply.fail(req.tag, E.INVAL); + if (!std.mem.eql(u8, std.mem.trim(u8, req.data, " \t\r\n"), "follow")) return Reply.fail(req.tag, E.INVAL); + slot.follow = true; + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// The length a stat reports: the text an open would freeze now. +pub fn logLen(q: *const Queue) u64 { + var n: u64 = 0; + var at = q.head; + while (at + 4 <= q.buf.items.len) { + const len = std.mem.readInt(u32, q.buf.items[at..][0..4], .little); + n += len; + at += 4 + len; + } + return n; +} + // ---- per-pane event records ---- pub const max_record_text = 256; @@ -517,49 +621,81 @@ test "writing an event record back performs the action it names" { try testing.expectEqual(@as(u8, 'F'), p.fs.origin); } -test "the log parks until a pane is created, renamed, saved or deleted" { +test "the log records whether or not anyone reads, and an open that follows waits" { const gpa = testing.allocator; const p = try withFile(gpa, "logged\n"); defer p.deinit(); const log = @intFromEnum(tree.TopFile.log); - _ = try th.newPane(p); - try testing.expect(p.fs.log.empty()); + var expected: [4200]u8 = undefined; + + // Nobody holds /log open, and the pane is recorded anyway. + const early = try th.newPane(p); + const early_line = try std.fmt.bufPrint(&expected, "new {d} {s}\n", .{ early, pane_files.nameOf(p.panes[p.paneBySerial(early).?].?) }); + const early_owned = try gpa.dupe(u8, early_line); + defer gpa.free(early_owned); - const opened = call(p, .{ .tag = 1, .op = .open, .node = log }); - try testing.expectEqual(Status.ok, opened.reply.status); - try testing.expectEqual(@as(u16, 1), p.fs.log_readers); - try testing.expectEqual(Status.again, rd(p, log, 0, 4096).reply.status); + // A plain open answers what happened so far, and ends: `cat log` never hangs. + const plain = call(p, .{ .tag = 1, .op = .open, .node = log }); + try testing.expectEqual(Status.ok, plain.reply.status); + const first = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = plain.reply.handle, .size = 8192 }); + try testing.expect(std.mem.endsWith(u8, first.bytes, early_owned)); + const past = call(p, .{ .tag = 3, .op = .read, .node = log, .handle = plain.reply.handle, .off = first.bytes.len, .size = 8192 }); + try testing.expectEqual(Status.ok, past.reply.status); + try testing.expectEqual(@as(usize, 0), past.bytes.len); + _ = call(p, .{ .tag = 4, .op = .release, .node = log, .handle = plain.reply.handle }); + + // `follow` on an open makes it wait past what it froze, one record a read. + const f = call(p, .{ .tag = 5, .op = .open, .node = log }); + const fh = f.reply.handle; + const frozen = call(p, .{ .tag = 6, .op = .read, .node = log, .handle = fh, .size = 8192 }).bytes.len; + try testing.expectEqual(Status.err, call(p, .{ .tag = 7, .op = .write, .node = log, .handle = fh, .data = "tail\n" }).reply.status); + try testing.expectEqual(Status.ok, call(p, .{ .tag = 8, .op = .write, .node = log, .handle = fh, .data = "follow\n" }).reply.status); + const rdf = struct { + fn next(pp: *Pardes, node: u64, h: u32, off: u64) th.Answer { + return call(pp, .{ .tag = 9, .op = .read, .node = node, .handle = h, .off = off, .size = 8192 }); + } + }; + try testing.expectEqual(Status.again, rdf.next(p, log, fh, frozen).reply.status); const serial = try th.newPane(p); const id = p.paneBySerial(serial).?; - var expected: [4200]u8 = undefined; try testing.expectEqualStrings( try std.fmt.bufPrint(&expected, "new {d} {s}\n", .{ serial, pane_files.nameOf(p.panes[id].?) }), - rd(p, log, 0, 4096).bytes, + rdf.next(p, log, fh, frozen).bytes, ); try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/logged.txt\n").reply.status); try testing.expectEqualStrings( try std.fmt.bufPrint(&expected, "rename {d} /tmp/logged.txt\n", .{serial}), - rd(p, log, 0, 4096).bytes, + rdf.next(p, log, fh, frozen).bytes, ); - const saving = wr(p, Node.of(serial, .exec), "Save\n"); - try testing.expectEqual(Status.ok, saving.reply.status); - try testing.expect(saving.saved); - p.perform(.{ .save_file = .{ .pane = @intCast(id) } }); + // What the editor says is in the same stream, one line even when the + // message had several. + p.setMessage(id, "save: two\nlines"); try testing.expectEqualStrings( - try std.fmt.bufPrint(&expected, "save {d} /tmp/logged.txt\n", .{serial}), - rd(p, log, 0, 4096).bytes, + try std.fmt.bufPrint(&expected, "msg {d} save: two lines\n", .{serial}), + rdf.next(p, log, fh, frozen).bytes, ); + // A builtin announces itself on the message row, so the log says which ran. try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Del\n").reply.status); try testing.expectEqualStrings( + try std.fmt.bufPrint(&expected, "msg {d} Del\n", .{serial}), + rdf.next(p, log, fh, frozen).bytes, + ); + try testing.expectEqualStrings( try std.fmt.bufPrint(&expected, "del {d} /tmp/logged.txt\n", .{serial}), - rd(p, log, 0, 4096).bytes, + rdf.next(p, log, fh, frozen).bytes, ); - try testing.expectEqual(Status.again, rd(p, log, 0, 4096).reply.status); + try testing.expectEqual(Status.again, rdf.next(p, log, fh, frozen).reply.status); - _ = call(p, .{ .tag = 2, .op = .release, .node = log, .handle = opened.reply.handle }); - try testing.expectEqual(@as(u16, 0), p.fs.log_readers); - _ = try th.newPane(p); - try testing.expect(p.fs.log.empty()); - try testing.expectEqual(@as(usize, 0), p.fs.log.buf.capacity); + // A follower the ring outran hears how much it missed, then carries on. + var filler: [200]u8 = @splat('x'); + for (0..p.fs.log.cap / filler.len + 8) |i| { + _ = std.fmt.bufPrint(&filler, "{d:0>6}", .{i}) catch unreachable; // a repeat would collapse + p.setMessage(0, &filler); + } + const lost = rdf.next(p, log, fh, frozen).bytes; + try testing.expect(std.mem.startsWith(u8, lost, "lost ")); + try testing.expect(std.mem.startsWith(u8, rdf.next(p, log, fh, frozen).bytes, "msg ")); + _ = call(p, .{ .tag = 10, .op = .release, .node = log, .handle = fh }); + for (p.fs.snapshots) |slot| try testing.expect(slot.node == 0); } diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig index 60fb5951..245a31ef 100644 --- a/src/ninep/screen.zig +++ b/src/ninep/screen.zig @@ -11,7 +11,13 @@ const Req = tree.Req; const Reply = tree.Reply; const E = tree.E; -pub const Snapshot = struct { node: u64 = 0, bytes: ?[]const u8 = null }; +pub const Snapshot = struct { + node: u64 = 0, + bytes: ?[]const u8 = null, + /// /log only: this open waits for records after `bytes`, from `next`. + follow: bool = false, + next: u64 = 0, +}; pub const snapshot_slots = 32; pub fn render(p: *Pardes) ![]u8 { diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index aca8d109..f98429ab 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -117,8 +117,8 @@ pub const TopFile = enum(u4) { pub fn mode(f: TopFile) u16 { return switch (f) { .root, .pane => 0o755, - .look, .exec => 0o666, - .README, .index, .status, .log, .screen, .listeners, .new => 0o444, + .look, .exec, .log => 0o666, + .README, .index, .status, .screen, .listeners, .new => 0o444, }; } @@ -375,7 +375,7 @@ fn topSize(p: *Pardes, f: TopFile) u64 { .README => fs.help.len, .status => ctl.statusLen(p), .look, .exec => ctl.resultsLen(p), - .log => events.pending(&p.fs.log), + .log => events.logLen(&p.fs.log), .listeners => screen.listenersLen(p), }; } @@ -497,7 +497,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { .top => |f| switch (f) { .new => return .{ .tag = req.tag, .handle = makePane(p) orelse return Reply.fail(req.tag, E.NFILE) }, .screen => return screen.openSnapshot(p, req, true), - .log => p.fs.log_readers +|= 1, + .log => return events.openLog(p, req), else => {}, }, .pane => |t| { @@ -543,10 +543,7 @@ fn releaseHandle(p: *Pardes, req: Req) Reply { if (screen.releaseSnapshot(p, req)) return .{ .tag = req.tag }; const target = Node.target(req.node) orelse return .{ .tag = req.tag }; switch (target) { - .top => |f| if (f == .log and p.fs.log_readers > 0) { - p.fs.log_readers -= 1; - if (p.fs.log_readers == 0) p.fs.log.clearAndFree(p.gpa); - }, + .top => {}, .pane => |t| { if (t.file != .event and t.file != .pty_data) return .{ .tag = req.tag }; const id = p.paneBySerial(t.serial) orelse return .{ .tag = req.tag }; @@ -597,7 +594,7 @@ fn read(p: *Pardes, req: Req, target: Target) Reply { }, .status => ctl.readStatus(p, req), .look, .exec => ctl.readResults(p, req), - .log => events.readQueue(p, req, &p.fs.log), + .log => events.readLog(p, req), .screen => screen.readSnapshot(p, req, null), .listeners => screen.readListeners(p, req), // The serial the open handed this fid, so that two reads of one @@ -623,6 +620,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { switch (target) { .top => |f| return switch (f) { .look, .exec => ctl.command(p, req, null, f == .exec), + .log => events.writeLog(p, req), else => Reply.fail(req.tag, E.PERM), }, .pane => |t| { @@ -730,6 +728,36 @@ test "filesystem pane creation and truncation cancel Look hover" { } } +test "README names every file the tree serves" { + // /README is the whole manual a client gets; a file it never mentions + // is one nobody can find without reading this source. + const words = struct { + fn has(name: []const u8) bool { + var it = std.mem.tokenizeAny(u8, fs.help, " \t\n,.:;()'\"$<>|=/"); + while (it.next()) |w| if (std.mem.eql(u8, w, name)) return true; + return false; + } + }; + inline for (@typeInfo(TopFile).@"enum".fields) |f| { + const t: TopFile = @enumFromInt(f.value); + if (t != .root and !words.has(t.fileName())) { + std.debug.print("README does not mention /{s}\n", .{t.fileName()}); + return error.Undocumented; + } + } + inline for (@typeInfo(PaneFile).@"enum".fields) |f| { + const t: PaneFile = @enumFromInt(f.value); + const ok = if (t == .dir) true else if (t.inPty() and t != .pty) + std.mem.indexOf(u8, fs.help, "pty/" ++ comptime @as(PaneFile, @enumFromInt(f.value)).fileName()) != null + else + words.has(t.fileName()); + if (!ok) { + std.debug.print("README does not mention pane file {s}\n", .{@tagName(t)}); + return error.Undocumented; + } + } +} + test "readdir lists the root and a pane directory without creating anything" { const gpa = testing.allocator; const p = try withFile(gpa, "hello\n"); diff --git a/src/panes.zig b/src/panes.zig index 0b4204aa..156d443a 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -3072,7 +3072,7 @@ pub const Output = struct { errdefer out.deinit(); var i: usize = 0; while (p.messageLog(i)) |m| : (i += 1) { - if (m.pane != 0xff) try out.writer.print("{d}: ", .{m.pane}); + if (m.serial != 0) try out.writer.print("{d}: ", .{m.serial}); try out.writer.writeAll(m.slice()); if (m.repeats > 1) try out.writer.print(" (x{d})", .{m.repeats}); try out.writer.writeByte('\n'); diff --git a/src/pardes.zig b/src/pardes.zig index ca09caa1..6d294230 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -672,9 +672,11 @@ test "the message log keeps what the row forgets, and collapses repeats" { // The same text from a DIFFERENT pane is a different event: one pane's // failure must not be recorded as another's. - p.setMessage(1, "save: AccessDenied"); + p.newScratchBelow(0); + const other = p.active; + p.setMessage(other, "save: AccessDenied"); try std.testing.expectEqual(@as(usize, 3), p.messages_len); - try std.testing.expectEqual(@as(u8, 1), p.messageLog(2).?.pane); + try std.testing.expectEqual(p.panes[other].?.serial, p.messageLog(2).?.serial); // Progress is NOT logged: it arrives several times a second for a whole // index and would push everything else out (`setStatus`). @@ -6254,8 +6256,10 @@ pub const LoggedMessage = struct { pub const cap = 256; text: [cap]u8 = undefined, len: u16 = 0, - /// Which pane said it, or 0xff for one said to no pane in particular. - pane: u8 = 0xff, + /// The serial of the pane that said it, or 0 for none in particular. A + /// serial, not a slot: slots are reused, so an old line would name + /// whichever pane took the slot since. + serial: u32 = 0, /// How many times in a row. Saturating: a key held down against the same /// refusal is one event, not four hundred. repeats: u16 = 0, @@ -7432,11 +7436,11 @@ pub const Pardes = struct { fn logMessage(p: *Pardes, id: usize, text: []const u8) void { if (text.len == 0) return; - const pane: u8 = if (id < MAX_PANES) @intCast(id) else 0xff; + const serial: u32 = if (id < MAX_PANES) if (p.panes[id]) |pane| pane.serial else 0 else 0; const kept = text[0..@min(text.len, LoggedMessage.cap)]; if (p.messages_len > 0) { const last = &p.messages[(p.messages_head + limits.message_log - 1) % limits.message_log]; - if (last.pane == pane and + if (last.serial == serial and std.mem.eql(u8, Message.body(last.slice()), Message.body(kept))) { // The NEWEST wording wins, so the row carries the latest clock @@ -7447,10 +7451,11 @@ pub const Pardes = struct { return; } } + ctlfs.events.noteMessage(p, serial, kept); const slot = &p.messages[p.messages_head]; slot.len = @intCast(kept.len); @memcpy(slot.text[0..slot.len], kept); - slot.pane = pane; + slot.serial = serial; slot.repeats = 1; p.messages_head = (p.messages_head + 1) % limits.message_log; p.messages_len = @min(p.messages_len + 1, limits.message_log); |
