summaryrefslogtreecommitdiff
path: root/src/ninep/events.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/events.zig')
-rw-r--r--src/ninep/events.zig322
1 files changed, 279 insertions, 43 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index ccc61352..98799774 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -110,7 +110,9 @@ pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply {
// ---- the editor-wide log ----
-pub const LogKind = enum { new, del, rename, save };
+/// `unsaved`: a pane a refusal (Exit, Restore, Del, Delcol) is about,
+/// logged before the write's err so a script reads which, whole.
+pub const LogKind = enum { new, del, rename, save, unsaved };
/// A pane leaving, before its state goes with it; one never announced
/// leaves silently.
@@ -122,6 +124,12 @@ pub fn noteRetire(p: *Pardes, pane: *Pane) void {
pane.fs.unannounced = false;
return;
}
+ // A question still standing ends with it, so a follower of its `ask`
+ // hears the end.
+ if (pane.prompt == .repl_choice or pane.prompt == .del_side or pane.prompt == .save) {
+ var rec: [32]u8 = undefined;
+ notePath(p, "answer", std.fmt.bufPrint(&rec, "{d} -", .{pane.serial}) catch "");
+ }
noteLog(p, .del, pane);
}
@@ -143,12 +151,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 {
+/// Text past LoggedMessage.cap bytes is cut between words with an ellipsis,
+/// as an err record's reason is.
+pub fn noteMessage(p: *Pardes, serial: u32, said: []const u8) void {
var buf: [pardes.Messages.LoggedMessage.cap + 32]u8 = undefined;
- pushLog(p, (if (serial == 0)
- std.fmt.bufPrint(&buf, "msg - {s}\n", .{text})
+ const kept = pardes.Messages.clip(said, pardes.Messages.LoggedMessage.cap);
+ const cut = if (kept.len < said.len) "…" else "";
+ // 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.
+ dropAnnouncement(p, serial, kept);
+ pushCounting(p, (if (serial == 0)
+ std.fmt.bufPrint(&buf, "msg - {s}{s}\n", .{ kept, cut })
else
- std.fmt.bufPrint(&buf, "msg {d} {s}\n", .{ serial, text })) catch return);
+ std.fmt.bufPrint(&buf, "msg {d} {s}{s}\n", .{ serial, kept, cut })) catch return);
}
/// Records `err <serial|-> <file>: <why>` for a write or truncation that was
@@ -165,16 +181,25 @@ pub fn notePath(p: *Pardes, what: []const u8, path: []const u8) void {
pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
const why = if (reply.ename.len > 0) reply.ename else cloud9.fs.errString(reply.errno);
- var name: [16]u8 = undefined;
+ var name: [32]u8 = undefined;
var serial: []const u8 = "-";
var file: []const u8 = "os";
if (req.node & tree.sources.archive_node != 0) file = "src";
if (req.node != pardes.filesystem.os_root and req.node & pardes.filesystem.os_node == 0 and req.node & tree.sources.archive_node == 0) {
if (tree.Node.target(req.node)) |t| switch (t) {
- .top => |f| file = f.fileName(),
+ .top => |f| {
+ file = f.fileName();
+ // The root's exec or look ran at the keyboard's pane: its
+ // failure is that pane's, as a click there would be.
+ if ((f == .exec or f == .look) and p.fs.ran_at != 0)
+ serial = std.fmt.bufPrint(&name, "{d}", .{p.fs.ran_at}) catch "-";
+ },
.col => |c| {
serial = "-";
- file = std.fmt.bufPrint(&name, "col/{d}/tag", .{c.serial}) catch "col";
+ file = (if (c.file == .dir)
+ std.fmt.bufPrint(&name, "col/{d}", .{c.serial})
+ else
+ std.fmt.bufPrint(&name, "col/{d}/{s}", .{ c.serial, @tagName(c.file) })) catch "col";
},
.pane => |pt| {
serial = std.fmt.bufPrint(&name, "{d}", .{pt.serial}) catch "-";
@@ -189,26 +214,91 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
};
}
var buf: [256]u8 = undefined;
- const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}\n", .{ serial, file, why[0..@min(why.len, 200)] }) catch return;
+ // A long reason is cut between words, with an ellipsis, never mid-word.
+ const kept = pardes.Messages.clip(why, 200);
+ const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}{s}\n", .{ serial, file, kept, if (kept.len < why.len) "…" else "" }) catch return;
for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') {
c.* = ' ';
};
+ pushCounting(p, record);
+}
+
+/// Drops the newest record when it is the Verbose announcement (`msg
+/// <serial> <Word>`) of the builtin whose words `text` are (`Word: ...`):
+/// what the builtin said, or the err its failure logs, takes its place.
+/// Not one a follower has read.
+pub fn dropAnnouncement(p: *Pardes, serial: u32, text: []const u8) void {
+ const last = newest(p) orelse return;
+ if (followerRead(p, last.seq) or !std.mem.startsWith(u8, last.text, "msg ")) return;
+ 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);
+}
+
+/// 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 {
+ const last = newest(p) orelse return;
+ if (followerRead(p, last.seq) or !std.mem.startsWith(u8, last.text, "msg ")) return;
+ const rest = last.text["msg ".len..];
+ const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse return;
+ if (std.mem.eql(u8, rest[sp + 1 ..], text)) p.fs.log.buf.shrinkRetainingCapacity(last.at);
+}
+
+/// Pushes `record`, or, when it repeats the newest record word for word,
+/// counts that one instead: `<record> (x2)`, the count being every time it
+/// was said. One a follower has already read is not rewritten: the repeat
+/// is a new line carrying the running count, `(x3)`.
+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 q = &p.fs.log;
- var at = q.head;
- var last: ?usize = null;
- var seq = q.dropped;
- while (at + 4 <= q.buf.items.len) : (seq += 1) {
- 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];
+ 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| {
@@ -217,14 +307,35 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
} else |_| {}
};
if (!std.mem.eql(u8, said, record[0 .. record.len - 1])) 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;
+ if (!followerRead(p, last.seq)) 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;
+ var seq = q.dropped;
+ while (at + 4 <= q.buf.items.len) : (seq += 1) {
+ last = at;
+ at += 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little);
+ }
+ 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
@@ -322,7 +433,12 @@ pub fn readLog(p: *Pardes, req: Req) Reply {
/// records instead of ending; closing it is the way back.
pub fn writeLog(p: *Pardes, req: Req) Reply {
const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.log;
- if (!std.mem.eql(u8, std.mem.trim(u8, req.data, " \t\r\n"), "follow")) return Reply.fail(req.tag, E.INVAL);
+ const word = std.mem.trim(u8, req.data, " \t\r\n");
+ // `follow` reads what the open froze, then waits; `follow new` skips
+ // what was there and waits for what comes after, as tail -n0 -f does.
+ if (std.mem.eql(u8, word, "follow new")) {
+ slot.pos = slot.bytes.len;
+ } else if (!std.mem.eql(u8, word, "follow")) return Reply.fail(req.tag, E.INVAL);
slot.follow = true;
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
@@ -459,6 +575,16 @@ pub fn noteAction(
return true;
}
+/// A record that follows another with no place of its own, `<c>0 0 0 <n>
+/// <text>`: a chorded exec's argument and its location (acme exec.c:182).
+pub fn noteFollow(p: *Pardes, id: usize, action: Action, text: []const u8) void {
+ const pane = p.panes[id] orelse return;
+ if (pane.fs.readers == 0) return;
+ var buf: [max_record_text + 64]u8 = undefined;
+ pane.fs.events.push(p.gpa, formatRecord(&buf, p.fs.origin, action, 0, 0, 0, text));
+ p.fs.news = true;
+}
+
pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void {
if (id >= MAX_PANES or bytes.len == 0) return;
const pf = &(p.panes[id] orelse return).fs;
@@ -472,7 +598,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void {
p.fs.news = true;
}
-const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, text: ?[]const u8 = null };
+const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, flag: u32 = 0, text: ?[]const u8 = null };
const EventReader = struct {
data: []const u8,
@@ -493,8 +619,9 @@ const EventReader = struct {
// is taken too, so a click with no place in the text (one written
// to a pane's own look or exec) can be written back and done.
var text: ?[]const u8 = null;
+ var flag: u32 = 0;
if (i < er.data.len and er.data[i] != '\n') {
- _ = scanNumber(er.data, &i) orelse return null;
+ flag = scanNumber(er.data, &i) orelse return null;
const n = scanNumber(er.data, &i) orelse return null;
if (i >= er.data.len or er.data[i] != ' ' or er.data.len - (i + 1) < n) return null;
text = er.data[i + 1 ..][0..n];
@@ -502,7 +629,7 @@ const EventReader = struct {
}
if (i >= er.data.len or er.data[i] != '\n') return null;
er.i = i + 1;
- return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .text = text };
+ return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .flag = flag, .text = text };
}
};
@@ -530,7 +657,9 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
else => return tree.failText(req.tag, E.INVAL, tree.e_bad_event),
}
const n = if (r.action.onTag()) tag.len else body.len;
- if (r.q0 > r.q1 or r.q1 > n) return tree.failText(req.tag, E.INVAL, tree.e_bad_event);
+ if (r.q0 > r.q1) return tree.failText(req.tag, E.INVAL, tree.e_bad_event);
+ // Well formed, but not a range of this text: say which.
+ if (r.q1 > n) return tree.failText(req.tag, E.INVAL, if (r.action.onTag()) "range past end of tag" else "range past end of body");
}
if (check.i != req.data.len) return tree.failText(req.tag, E.INVAL, tree.e_bad_event);
}
@@ -548,9 +677,65 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
// been (acme's xfideventwrite, xfid.c:842-844): a REPL bound for
// it takes it. One that came from a write to exec (`F`) was
// never a click, and runs as the command it was.
- .body_exec => _ = exec.executeFrom(p, live, text, r.origin != 'F'),
- .tag_exec => _ = exec.execute(p, live, text),
- .body_look, .tag_look => look.lookAt(p, live, text),
+ // The chorded exec a reader was told of runs with its argument
+ // (mouse.reportGesture), when it is that range written back.
+ .body_exec, .tag_exec => {
+ const pf = &pane.fs;
+ // The two records that follow a chorded exec (its argument
+ // and where it came from), written back after it, are its:
+ // consumed, never run as commands of their own.
+ if (pf.chord_follow > 0 and r.q0 == 0 and r.q1 == 0 and r.flag == 0) {
+ pf.chord_follow -= 1;
+ // The argument a command written back alone waited for.
+ if (pf.chord_pending) |command| {
+ pf.chord_pending = null;
+ defer p.gpa.free(command);
+ const line = exec.withArg(p, command, r.text orelse "");
+ if (pf.chord_pending_tag) _ = exec.execute(p, live, line) else _ = exec.executeFrom(p, live, line, true);
+ }
+ continue;
+ }
+ var arg = if (pf.chord_arg) |a| (if (pf.chord_at.q0 == r.q0 and pf.chord_at.q1 == r.q1) a else null) else null;
+ if (r.flag & flag_chorded != 0) {
+ // Its argument is the next record when it came in the
+ // same write; the rest that follow are consumed.
+ pf.chord_follow = 2;
+ const save = run.i;
+ if (run.next()) |following| if (following.q0 == 0 and following.q1 == 0 and following.flag == 0) {
+ if (following.text) |t| arg = p.scratch.allocator().dupe(u8, t) catch arg;
+ pf.chord_follow = 1;
+ } else {
+ run.i = save;
+ };
+ // Alone, with no argument kept: it waits for its own.
+ if (arg == null) {
+ if (pf.chord_pending) |old| p.gpa.free(old);
+ pf.chord_pending = p.gpa.dupe(u8, text) catch null;
+ pf.chord_pending_tag = r.action == .tag_exec;
+ continue;
+ }
+ }
+ if (r.action == .tag_exec)
+ _ = exec.execute(p, live, exec.withArg(p, text, arg))
+ else
+ _ = exec.executeFrom(p, live, exec.withArg(p, text, arg), r.origin != 'F');
+ if (arg != null) if (p.paneBySerial(serial)) |still| {
+ const spf = &p.panes[still].?.fs;
+ if (spf.chord_arg) |a| p.gpa.free(a);
+ spf.chord_arg = null;
+ };
+ },
+ // What it found is what the look file reads back next, as a
+ // look written there would leave (ctl.run).
+ .body_look, .tag_look => {
+ p.look_missed = false;
+ look.lookAt(p, live, text);
+ p.fs.results_len = 0;
+ if (!p.look_missed) if (p.panes[p.active]) |found| {
+ p.fs.results[0] = found.serial;
+ p.fs.results_len = 1;
+ };
+ },
else => unreachable,
}
}
@@ -726,7 +911,7 @@ test "a pane deleted while its event file is open leaves no suppression behind"
try testing.expectEqual(@as(u16, 0), p.fs.listeners);
// The reader's next read hears what acme says of a window gone under it.
const shut = call(p, .{ .tag = 19, .op = .read, .node = event, .handle = a.reply.handle, .size = 64 });
- try testing.expectEqual(E.IO, shut.errno());
+ try testing.expectEqual(E.NOENT, shut.errno());
try testing.expectEqualStrings(tree.e_shut_down, shut.reply.ename);
_ = call(p, .{ .tag = 20, .op = .release, .node = event, .handle = a.reply.handle });
@@ -767,6 +952,12 @@ test "writing an event record back performs the action it names" {
try testing.expectEqual(E.INVAL, wr(p, event, bad).errno());
try testing.expectEqual(@as(usize, 0), pane.msg_len);
}
+ // A look written back sets what look reads back.
+ _ = wr(p, event, "FL0 3\n");
+ var want_serial: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want_serial, "{d}\n", .{serial}), rd(p, @intFromEnum(tree.TopFile.look), 0, 64).bytes);
+ // Well formed but past the text: said so, not "bad event syntax".
+ try testing.expectEqualStrings("range past end of body", wr(p, event, "MX0 999\n").reply.ename);
_ = call(p, .{ .tag = 23, .op = .open, .node = event });
p.fs.origin = 'K';
@@ -848,10 +1039,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') {
@@ -860,7 +1051,8 @@ 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]);
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), rdf.next(p, log, fh, frozen).bytes);
+ p.setMessage(0, "same failure");
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure (x2)\n", .{said_by}), rdf.next(p, log, fh, frozen).bytes);
// A follower the ring outran hears how much it missed, then carries on.
var filler: [200]u8 = @splat('x');
@@ -911,19 +1103,20 @@ test "a refused or failed write is an err record in the log, saying which file a
// A write that succeeds says nothing.
_ = wr(p, Node.of(serial, .addr), "#0");
try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status);
- // A follower that has read an err waits on its repeat: a line of its own.
+ // A follower that has read an err waits on its repeat: a line of its
+ // own, with the running count.
_ = wr(p, Node.of(serial, .addr), "/nowhere/");
try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen));
_ = wr(p, Node.of(serial, .addr), "/nowhere/");
- try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen));
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp (x2)\n", .{serial}), next.record(p, f, frozen));
_ = call(p, .{ .tag = 6, .op = .release, .node = log, .handle = f });
- // With the follower gone, repeats are the newest record counted: the
- // one it read, and three more.
+ // With the follower gone, repeats are the newest record counted on:
+ // its two, and three more.
for (0..3) |_| _ = wr(p, Node.of(serial, .addr), "/elsewhere/");
const g = call(p, .{ .tag = 7, .op = .open, .node = log }).reply.handle;
const text = call(p, .{ .tag = 8, .op = .read, .node = log, .handle = g, .size = 1 << 16 }).bytes;
- try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "regexp\nerr {d} addr: no match for regexp (x4)\n", .{serial})));
+ try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "regexp\nerr {d} addr: no match for regexp (x5)\n", .{serial})));
_ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = g });
}
@@ -1020,3 +1213,46 @@ test "a click in the body reports the offsets of the word it takes, so the recor
_ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h });
}
}
+
+test "follow new skips what the log held and waits for what comes after" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ p.update(.tick); // the pane's `new` is logged first
+ while (p.nextEffect()) |_| {}
+ p.setMessage(0, "before the open");
+ const log = @intFromEnum(tree.TopFile.log);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = log, .handle = h, .data = "follow new\n" }).reply.status);
+ try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = log, .handle = h, .size = 4096 }).reply.status);
+ p.setMessage(0, "after it");
+ try testing.expect(std.mem.endsWith(u8, call(p, .{ .tag = 4, .op = .read, .node = log, .handle = h, .size = 4096 }).bytes, " after it\n"));
+ _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h });
+}
+
+test "a long msg record is cut between words at its cap, with an ellipsis" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const long = "word " ** 60; // 300 bytes
+ _ = wr(p, Node.of(serialOf(p), .ctl), "Msg " ++ long ++ "\n");
+ const log = @intFromEnum(tree.TopFile.log);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes;
+ const at = std.mem.lastIndexOf(u8, text, "msg ").?;
+ const record = text[at .. std.mem.indexOfScalarPos(u8, text, at, '\n').? + 1];
+ try testing.expect(std.mem.endsWith(u8, record, " word…\n"));
+ try testing.expect(record.len <= "msg 1 ".len + pardes.Messages.LoggedMessage.cap + "…\n".len);
+ _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
+}
+
+test "a long err record is cut between words, with an ellipsis" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus " ++ "abcdefgh " ** 40 ++ "\n");
+ const log = @intFromEnum(tree.TopFile.log);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes;
+ const at = std.mem.lastIndexOf(u8, text, "err - ctl: ").?;
+ const record = text[at .. std.mem.indexOfScalarPos(u8, text, at, '\n').? + 1];
+ try testing.expect(std.mem.endsWith(u8, record, "abcdefgh…\n"));
+ _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
+}