summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ninep/ctl.zig22
-rw-r--r--src/ninep/events.zig7
-rw-r--r--src/ninep/pane.zig67
-rw-r--r--src/ninep/pty.zig5
-rw-r--r--src/ninep/screen.zig9
-rw-r--r--src/ninep/tree.zig100
6 files changed, 72 insertions, 138 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 14f1ae4f..945119ca 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -38,11 +38,6 @@ const Serials = struct {
};
return s;
}
-
- fn has(s: *const Serials, serial: u32) bool {
- for (s.list[0..s.len]) |known| if (known == serial) return true;
- return false;
- }
};
fn noteResult(p: *Pardes, serial: u32) void {
@@ -52,11 +47,6 @@ fn noteResult(p: *Pardes, serial: u32) void {
p.fs.results_len += 1;
}
-fn activeSerial(p: *Pardes) ?u32 {
- const pane = p.panes[p.active] orelse return null;
- return pane.serial;
-}
-
/// Runs one look or exec from the pane `id`, then records the panes it
/// created; failing that, a look records the pane it focused and an exec
/// the pane it acted on (even one it closed).
@@ -66,12 +56,12 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
if (exec) _ = pardes.exec.execute(p, id, text) else look.lookAt(p, id, text);
const after = Serials.snapshot(p);
var created = false;
- for (after.list[0..after.len]) |serial| if (!before.has(serial)) {
+ for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) {
noteResult(p, serial);
created = true;
};
if (created) return;
- noteResult(p, if (exec) context else activeSerial(p) orelse return);
+ noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial);
}
// ---- look and exec ----
@@ -140,11 +130,6 @@ pub fn readStatus(p: *Pardes, req: Req) Reply {
// ---- /pane/<n>/ctl ----
-fn fontName(p: *Pardes) []const u8 {
- const name = p.settings.font.effective_name.get();
- return if (name.len == 0) "default" else name;
-}
-
fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void {
const plain = s.len > 0 and for (s) |c| {
if (c <= ' ' or c == '\'') break false;
@@ -175,7 +160,8 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
@intFromBool(pane_files.dirtyOf(pane)),
pane.cols,
}) catch return w.buffered();
- writeQuoted(&w, fontName(p)) catch return w.buffered();
+ const font = p.settings.font.effective_name.get();
+ writeQuoted(&w, if (font.len == 0) "default" else font) catch return w.buffered();
w.print(" {d:>11} {s} ", .{ config.tab_width, if (p.panes[p.active] == pane) "current" else "notcurrent" }) catch {};
return w.buffered();
}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 49e852b3..a501d1e2 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -96,13 +96,6 @@ pub const Queue = struct {
}
};
-/// What the next read would answer, which is the size a stat reports: a
-/// client can see there is something waiting without parking on it.
-pub fn pending(q: *const Queue) u64 {
- const record = q.peek() orelse return 0;
- return record.len;
-}
-
/// One record per read; `.again` holds the read until a record arrives.
pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply {
const record = q.peek() orelse return .{ .tag = req.tag, .status = .again };
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 64355e0e..1e549107 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -83,12 +83,6 @@ pub fn tagOf(p: *Pardes, pane: *Pane) []const u8 {
return tagline.tagText(p, p.scratch.allocator(), pane) catch "";
}
-pub fn dirOf(pane: *Pane) []const u8 {
- if (pane.file) |*f| return std.fs.path.dirname(f.path) orelse "/";
- const cwd = pane.cwdSlice();
- return if (cwd.len > 0) cwd else "/";
-}
-
pub fn dirtyOf(pane: *const Pane) bool {
const f = if (pane.file) |*x| x else return false;
if (!panes.Output.fileTraits(f.output).saves) return false;
@@ -111,24 +105,15 @@ pub fn kindOf(pane: *const Pane) []const u8 {
return "term";
}
-pub fn mtimeOf(p: *Pardes, pane: *const Pane) u32 {
- if (pane.file) |*f| if (f.mtime != 0) return f.mtime;
- return p.fs.started;
-}
-
pub fn clip(n: usize) u32 {
return std.math.cast(u32, n) orelse std.math.maxInt(u32);
}
-fn cellOf(row: i32, col: i32) modal.Cursor {
- return .{ .row = @intCast(@max(0, row)), .col = @intCast(@max(0, col)) };
-}
-
pub fn dotOf(pane: *Pane) State.Range {
const text = bodyOf(pane);
- const head = modal.offsetAt(text, cellOf(pane.body.cur_row, pane.body.cur_col));
+ const head = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) });
if (!pane.body.vsel.active) return .{ .q0 = clip(head), .q1 = clip(head) };
- const anchor = modal.offsetAt(text, cellOf(pane.body.vsel.row, pane.body.vsel.col));
+ const anchor = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) });
var hi = @max(head, anchor);
if (hi < text.len) hi = modal.nextGrapheme(text, hi);
return .{ .q0 = clip(@min(head, anchor)), .q1 = clip(hi) };
@@ -169,10 +154,6 @@ pub fn clampAddr(pf: *State, len: usize) void {
}
}
-fn shiftBy(r: State.Range, at: u32, removed: u32, inserted: u32) State.Range {
- return .{ .q0 = shiftOne(r.q0, at, removed, inserted), .q1 = shiftOne(r.q1, at, removed, inserted) };
-}
-
fn shiftOne(v: u32, at: u32, removed: u32, inserted: u32) u32 {
if (v <= at) return v;
if (v <= at +| removed) return at +| inserted;
@@ -234,30 +215,21 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 {
.limit => if (pf.limit == null) 0 else range_len,
.dirty, .mark, .scroll => 2,
.look, .exec => ctl.resultsLen(p),
- .event => events.pending(&pf.events),
+ // What the next read would answer, which a client can see without
+ // parking on it.
+ .event => if (pf.events.peek()) |record| record.len else 0,
.pty_status => pty.status_len,
- .pty_ctl => pty.ctlLen(pane),
- .pty_data => events.pending(&pf.pty_out),
+ .pty_ctl => pty_ctl: {
+ var buf: [32]u8 = undefined;
+ break :pty_ctl (std.fmt.bufPrint(&buf, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch unreachable).len;
+ },
+ .pty_data => if (pf.pty_out.peek()) |chunk| chunk.len else 0,
.dir, .errors, .pty, .pty_run => 0,
};
}
-/// The qid version, which a client polls to watch a file for change. Only
-/// the pane's text can promise one: its revision counts every edit, and
-/// nothing else here changes in step with it.
-pub fn versionOf(pane: *const Pane, f: PaneFile) u32 {
- return switch (f) {
- .body, .data, .xdata => if (pane.file) |*file| file.revision else 0,
- else => 0,
- };
-}
-
// ---- the index ----
-pub fn stageIndexLine(p: *Pardes, out: *std.ArrayList(u8), pane: *Pane) void {
- out.print(p.gpa, "{d} {s} {d} {s}\n", .{ pane.serial, kindOf(pane), @intFromBool(dirtyOf(pane)), nameOf(pane) }) catch {};
-}
-
pub fn indexLen(p: *Pardes) u64 {
var n: u64 = 0;
var last: u32 = 0;
@@ -440,7 +412,10 @@ fn writeData(p: *Pardes, req: Req, pane: *Pane) Reply {
const before = dotOf(pane);
const take = spliceBody(p, pane, q0, q1, req.data) orelse
return Reply.fail(req.tag, E.NOMEM);
- setDot(pane, shiftBy(before, clip(q0), clip(q1 - q0), clip(take)));
+ setDot(pane, .{
+ .q0 = shiftOne(before.q0, clip(q0), clip(q1 - q0), clip(take)),
+ .q1 = shiftOne(before.q1, clip(q0), clip(q1 - q0), clip(take)),
+ });
pf.addr = .{ .q0 = clip(q0 + take), .q1 = clip(q0 + take) };
if (!pf.noscroll) showOffset(pane, q0 + take);
return .{ .tag = req.tag, .written = @intCast(take) };
@@ -497,15 +472,10 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-fn flagOf(data: []const u8) ?bool {
- const word = std.mem.trim(u8, data, " \t\r\n");
- if (std.mem.eql(u8, word, "1")) return true;
- if (std.mem.eql(u8, word, "0")) return false;
- return null;
-}
-
fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply {
- const on = flagOf(req.data) orelse return Reply.fail(req.tag, E.INVAL);
+ const word = std.mem.trim(u8, req.data, " \t\r\n");
+ if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return Reply.fail(req.tag, E.INVAL);
+ const on = word[0] == '1';
const pf = &pane.fs;
switch (file) {
.dirty => {
@@ -577,7 +547,8 @@ pub fn nameBuffer(p: *Pardes, id: usize, full: []const u8, preserve_tag: bool) !
pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize {
if (text.len == 0) return 0;
const pane = p.panes[id] orelse return null;
- const dir = dirOf(pane);
+ const cwd = pane.cwdSlice();
+ const dir = if (pane.file) |*f| std.fs.path.dirname(f.path) orelse "/" else if (cwd.len > 0) cwd else "/";
for (p.panes) |slot| {
const q = slot orelse continue;
const qf = fileOf(q) orelse continue;
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 49f52c82..eda6e9d0 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -84,11 +84,6 @@ pub fn readCtl(p: *Pardes, req: Req, pane: *Pane) Reply {
return tree.stagedReply(p, req);
}
-pub fn ctlLen(pane: *const Pane) u64 {
- var buf: [32]u8 = undefined;
- return (std.fmt.bufPrint(&buf, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch unreachable).len;
-}
-
/// Three right-aligned fields: cols, rows and whether the host holds the tty.
pub const status_len: u64 = 36;
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index f72043d4..bc2788ec 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -57,9 +57,9 @@ pub fn readSnapshot(p: *Pardes, req: Req, pane: ?*Pane) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(len) } };
}
-const listeners_cap = 512;
+pub const listeners_cap = 512;
-fn listenersText(p: *Pardes, buf: []u8) []const u8 {
+pub fn listenersText(p: *Pardes, buf: []u8) []const u8 {
var text = std.Io.Writer.fixed(buf);
if (p.fs.socket_path.len != 0)
text.print("unix!{s}\n", .{p.fs.socket_path}) catch return text.buffered();
@@ -75,11 +75,6 @@ fn listenersText(p: *Pardes, buf: []u8) []const u8 {
return text.buffered();
}
-pub fn listenersLen(p: *Pardes) u64 {
- var buf: [listeners_cap]u8 = undefined;
- return listenersText(p, &buf).len;
-}
-
pub fn readListeners(p: *Pardes, req: Req) Reply {
var buf: [listeners_cap]u8 = undefined;
const out = p.fs.stage(p.gpa);
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 53472a60..27ad02c8 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -66,10 +66,6 @@ pub const e_bad_event = "bad event syntax";
pub const e_in_use = "file in use";
pub const e_shut_down = "window shut down";
-fn reads(omode: u8) bool {
- return omode & 3 != 1; // OWRITE is the one access mode that never reads
-}
-
/// Tremove closes a pane; nothing else in the tree is created or destroyed by
/// the protocol, and wstat stays a truncation. Making a pane is an open of
/// /pane/new, which needs no feature of the engine's, so Tcreate is refused
@@ -281,18 +277,6 @@ pub const Target = union(enum) {
pane: struct { serial: u32, file: PaneFile },
};
-fn paneFileNamed(name: []const u8) ?PaneFile {
- const f = std.meta.stringToEnum(PaneFile, name) orelse return null;
- if (f == .dir) return null;
- return if (f.inPty() and f != .pty) null else f;
-}
-
-fn topFileNamed(name: []const u8) ?TopFile {
- const f = std.meta.stringToEnum(TopFile, name) orelse return null;
- // `new` is reached in /pane, where the panes it makes are.
- return if (f == .root or f == .new) null else f;
-}
-
/// Opening /pane/new makes a pane and reading the open fid answers its
/// serial, which is /net/tcp/clone's mechanism exactly (kernel/network/ip/
/// devip.c, `case Qclone` in ipopen, whose Qctl read prints the number it
@@ -305,13 +289,6 @@ fn topFileNamed(name: []const u8) ?TopFile {
/// can never collide with one.
pub const new_pane = "new";
-fn serialNamed(name: []const u8) ?u32 {
- if (name.len == 0 or name.len > 10) return null;
- for (name) |c| if (c < '0' or c > '9') return null;
- const serial = std.fmt.parseInt(u32, name, 10) catch return null;
- return if (serial == 0) null else serial;
-}
-
pub fn nextSerialAfter(p: *Pardes, after: u32) ?u32 {
var best: ?u32 = null;
for (p.panes) |slot| {
@@ -329,8 +306,9 @@ pub fn resolveSelf(p: *Pardes, path: []const u8) ?u64 {
var parts = std.mem.tokenizeScalar(u8, normalized, '/');
const first = parts.next() orelse return root;
if (!std.mem.eql(u8, first, "pane")) {
- const top = topFileNamed(first) orelse return sources.node(normalized);
- if (top == .pane or parts.next() != null) return null;
+ // `new` is reached in /pane, where the panes it makes are.
+ const top = std.meta.stringToEnum(TopFile, first) orelse return sources.node(normalized);
+ if (top == .root or top == .new or top == .pane or parts.next() != null) return null;
return @intFromEnum(top);
}
const next = parts.next() orelse return @intFromEnum(TopFile.pane);
@@ -338,9 +316,15 @@ pub fn resolveSelf(p: *Pardes, path: []const u8) ?u64 {
// path: this resolves Look targets and hover previews, and a preview that
// opened `new` to see what was there would make a pane per hover.
if (std.mem.eql(u8, next, new_pane)) return null;
- const serial = serialNamed(next) orelse return null;
+ // A pane directory is its serial, in decimal digits and never 0.
+ if (next.len > 10) return null;
+ for (next) |c| if (c < '0' or c > '9') return null;
+ const serial = std.fmt.parseInt(u32, next, 10) catch return null;
+ if (serial == 0) return null;
const id = p.paneBySerial(serial) orelse return null;
- const file = paneFileNamed(parts.next() orelse return Node.of(serial, .dir)) orelse return null;
+ // The pty files are reached inside pty/, not beside it.
+ const file = std.meta.stringToEnum(PaneFile, parts.next() orelse return Node.of(serial, .dir)) orelse return null;
+ if (file == .dir or (file.inPty() and file != .pty)) return null;
if (file.inPty() and !p.panes[id].?.isTerminal()) return null;
if (parts.next()) |name| {
if (file != .pty) return null;
@@ -390,7 +374,7 @@ pub fn handle(p: *Pardes, req: Req) Reply {
};
return switch (req.op) {
.lookup => lookup(p, req, target),
- .getattr => attrReply(p, req.tag, target),
+ .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) },
.setattr => setattr(p, req, target),
.open => open(p, req, target),
.release => release(p, req),
@@ -420,8 +404,14 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
.dir = t.file.isDir(),
.mode = t.file.mode(),
.size = pane.fileSize(p, id, t.file),
- .mtime = pane.mtimeOf(p, pn),
- .version = pane.versionOf(pn, t.file),
+ .mtime = if (pn.file) |*f| (if (f.mtime != 0) f.mtime else p.fs.started) else p.fs.started,
+ // The qid version, which a client polls to watch a file for
+ // change. Only the pane's text can promise one: its revision
+ // counts every edit, and nothing else here changes in step.
+ .version = switch (t.file) {
+ .body, .data, .xdata => if (pn.file) |*f| f.revision else 0,
+ else => 0,
+ },
// A write to body or tag appends whatever its offset says,
// which is why acme's dirtab marks both DMAPPEND
// (editors/acme/fsys.c:78).
@@ -431,11 +421,6 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
}
}
-fn attrReply(p: *Pardes, tag: u64, target: Target) Reply {
- const a = attrOf(p, target) orelse return Reply.fail(tag, E.NOENT);
- return .{ .tag = tag, .attr = a };
-}
-
fn topSize(p: *Pardes, f: TopFile) u64 {
return switch (f) {
// ponytail: /screen has no length until an open renders its frame,
@@ -446,7 +431,10 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
.status => ctl.statusLen(p),
.look, .exec => ctl.resultsLen(p),
.log => events.logLen(&p.fs.log),
- .listeners => screen.listenersLen(p),
+ .listeners => listeners: {
+ var buf: [screen.listeners_cap]u8 = undefined;
+ break :listeners screen.listenersText(p, &buf).len;
+ },
.ctl => ctl.rootLen(p),
.commands => ctl.commandsLen(),
.focus => focus: {
@@ -475,13 +463,18 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply {
.top => |f| switch (f) {
.root => root: {
if (std.mem.eql(u8, name, "os")) return fs.osHandle(p, .{ .tag = req.tag, .op = .getattr, .node = fs.os_root });
- if (topFileNamed(name)) |t| break :root @intFromEnum(t);
+ // `new` is reached in /pane, where the panes it makes are.
+ if (std.meta.stringToEnum(TopFile, name)) |t| if (t != .root and t != .new) break :root @intFromEnum(t);
if (sources.node(name)) |n| return sources.handle(p, .{ .tag = req.tag, .op = .getattr, .node = n });
return Reply.fail(req.tag, E.NOENT);
},
.pane => pane: {
if (std.mem.eql(u8, name, new_pane)) break :pane @intFromEnum(TopFile.new);
- const serial = serialNamed(name) orelse return Reply.fail(req.tag, E.NOENT);
+ // A pane directory is its serial, in decimal digits and never 0.
+ if (name.len > 10) return Reply.fail(req.tag, E.NOENT);
+ for (name) |c| if (c < '0' or c > '9') return Reply.fail(req.tag, E.NOENT);
+ const serial = std.fmt.parseInt(u32, name, 10) catch return Reply.fail(req.tag, E.NOENT);
+ if (serial == 0) return Reply.fail(req.tag, E.NOENT);
break :pane Node.of(serial, .dir);
},
else => return Reply.fail(req.tag, E.NOTDIR),
@@ -489,14 +482,15 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply {
.pane => |t| pane: {
_ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
const f = switch (t.file) {
- .dir => paneFileNamed(name),
+ .dir => if (std.meta.stringToEnum(PaneFile, name)) |f| (if (f == .dir or (f.inPty() and f != .pty)) null else f) else null,
.pty => pty.fileNamed(name),
else => return Reply.fail(req.tag, E.NOTDIR),
} orelse return Reply.fail(req.tag, E.NOENT);
break :pane Node.of(t.serial, f);
},
};
- return attrReply(p, req.tag, Node.target(node) orelse return Reply.fail(req.tag, E.NOENT));
+ const found = Node.target(node) orelse return Reply.fail(req.tag, E.NOENT);
+ return .{ .tag = req.tag, .attr = attrOf(p, found) orelse return Reply.fail(req.tag, E.NOENT) };
}
fn stagePaneFiles(p: *Pardes, out: *std.ArrayList(u8), serial: u32, terminal: bool, skip: *u64) void {
@@ -564,20 +558,19 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
}
-/// Opens a pane for an open of /pane/new, whose serial becomes the handle:
-/// the fid remembers which pane it made, so reading it twice answers the
-/// same one and closing it leaves the pane alone.
-fn makePane(p: *Pardes) ?u32 {
- const slot = p.freeSlot() orelse return null;
- p.newScratchBelow(p.active);
- return (p.panes[slot] orelse return null).serial;
-}
-
fn open(p: *Pardes, req: Req, target: Target) Reply {
var pane_of: ?*Pane = null;
const what: @FieldType(Open, "what") = switch (target) {
.top => |f| switch (f) {
- .new => return .{ .tag = req.tag, .handle = makePane(p) orelse return Reply.fail(req.tag, E.NFILE) },
+ // The made pane's serial is the handle: the fid remembers which
+ // pane it made, so reading it twice answers the same one and
+ // closing it leaves the pane alone.
+ .new => {
+ const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE);
+ p.newScratchBelow(p.active);
+ const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE);
+ return .{ .tag = req.tag, .handle = made.serial };
+ },
.screen => .{ .snapshot = null },
.log => .{ .log = .{} },
else => return .{ .tag = req.tag },
@@ -586,7 +579,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
const pn = p.panes[p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT)].?;
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
pane_of = pn;
- const reader = reads(req.omode);
+ const reader = req.omode & 3 != 1; // OWRITE is the one mode that never reads
break :what switch (t.file) {
.body => if (pn.isTerminal()) .{ .snapshot = null } else return .{ .tag = req.tag },
// Reads of these consume, so two readers would each see half
@@ -693,7 +686,7 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply {
},
.top => {},
};
- return attrReply(p, req.tag, target);
+ return .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) };
}
fn read(p: *Pardes, req: Req, target: Target) Reply {
@@ -710,7 +703,8 @@ fn read(p: *Pardes, req: Req, target: Target) Reply {
var last: u32 = 0;
while (nextSerialAfter(p, last)) |serial| {
last = serial;
- pane.stageIndexLine(p, out, p.panes[p.paneBySerial(serial).?].?);
+ const pn = p.panes[p.paneBySerial(serial).?].?;
+ out.print(p.gpa, "{d} {s} {d} {s}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn) }) catch {};
}
break :index stagedReply(p, req);
},