diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 00:01:04 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 64b6a0dbdaab4b314ccbce667709d71a8561daf0 (patch) | |
| tree | ceb955449a4458675e7d3bccca4565bd469039da /src/ninep/tree.zig | |
| parent | b71a9051006f88052d240e521046428c4e1c5e15 (diff) | |
| download | pardes-64b6a0dbdaab4b314ccbce667709d71a8561daf0.tar.gz pardes-64b6a0dbdaab4b314ccbce667709d71a8561daf0.zip | |
The tree serves the layout: /layout, /tag and /col/<n>/tag
A script could not see where the panes sit or edit the tags a person
clicks in. /layout lists each column, its index, x and width in cells,
current or not, empty or full, and its panes' serials, then the active
column; /tag is the workspace tag and /col/<n>/tag a column's, read and
written as a pane tag is (one line: a newline written in is a space, and a
truncating write drops the one ending it); /index's lines end with the
pane's column; the log says newcol <n> and delcol <n>. acme serves no
column files, and the docs say so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/tree.zig')
| -rw-r--r-- | src/ninep/tree.zig | 111 |
1 files changed, 93 insertions, 18 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 7279b64a..9459be4f 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -18,6 +18,7 @@ pub const events = @import("events.zig"); pub const pane = @import("pane.zig"); pub const addr = @import("addr.zig"); pub const ctl = @import("ctl.zig"); +pub const cols = @import("cols.zig"); pub const pty = @import("pty.zig"); pub const sources = @import("sources.zig"); pub const screen = @import("screen.zig"); @@ -171,7 +172,7 @@ pub fn openOf(p: *Pardes, req: Req) ?*Open { // ---- nodes ---- -pub const TopFile = enum(u4) { +pub const TopFile = enum(u5) { root = 1, README, index, @@ -186,6 +187,9 @@ pub const TopFile = enum(u4) { focus, ctl, commands, + layout, + tag, + col, pub fn fileName(f: TopFile) []const u8 { return if (f == .root) "/" else @tagName(f); @@ -193,14 +197,14 @@ pub const TopFile = enum(u4) { pub fn mode(f: TopFile) u16 { return switch (f) { - .root, .pane => 0o755, - .look, .exec, .log, .focus, .ctl => 0o666, - .README, .index, .status, .screen, .listeners, .new, .commands => 0o444, + .root, .pane, .col => 0o755, + .look, .exec, .log, .focus, .ctl, .tag => 0o666, + .README, .index, .status, .screen, .listeners, .new, .commands, .layout => 0o444, }; } pub fn dir(f: TopFile) bool { - return f == .root or f == .pane; + return f == .root or f == .pane or f == .col; } }; @@ -264,6 +268,11 @@ pub const PaneFile = enum(u5) { } }; +/// /col/<n> and its tag: a column is named by its index, which a serial +/// never is (serials are 32 bits; this bit is past them). +pub const ColFile = enum(u5) { dir = 0, tag = 1 }; +const col_bit: u59 = 1 << 58; + pub const Node = packed struct(u64) { file: u5 = 0, serial: u59 = 0, @@ -273,11 +282,19 @@ pub const Node = packed struct(u64) { return @bitCast(Node{ .file = @intFromEnum(file), .serial = serial }); } + pub fn ofCol(index: usize, file: ColFile) u64 { + return @bitCast(Node{ .file = @intFromEnum(file), .serial = col_bit | @as(u59, @intCast(index)) }); + } + pub fn target(node: u64) ?Target { const n: Node = @bitCast(node); if (n.serial == 0) { return .{ .top = std.enums.fromInt(TopFile, n.file) orelse return null }; } + if (n.serial & col_bit != 0) return .{ .col = .{ + .index = std.math.cast(u32, n.serial & ~col_bit) orelse return null, + .file = std.enums.fromInt(ColFile, n.file) orelse return null, + } }; return .{ .pane = .{ .serial = std.math.cast(u32, n.serial) orelse return null, .file = std.enums.fromInt(PaneFile, n.file) orelse return null, @@ -288,6 +305,7 @@ pub const Node = packed struct(u64) { pub const Target = union(enum) { top: TopFile, pane: struct { serial: u32, file: PaneFile }, + col: struct { index: u32, file: ColFile }, }; /// Opening /pane/new makes a pane and reading the open fid answers its @@ -388,7 +406,7 @@ pub fn handle(p: *Pardes, req: Req) Reply { const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT); if (req.op == .write or req.op == .setattr) p.fs.origin = switch (target) { .pane => |t| @as(u8, if (t.file == .body or t.file == .tag) 'E' else 'F'), - .top => 'F', + .top, .col => 'F', }; return switch (req.op) { .lookup => lookup(p, req, target), @@ -410,7 +428,7 @@ pub fn handle(p: *Pardes, req: Req) Reply { fn runAttr(p: *Pardes, req: Req, target: Target) ?Reply.Attr { const t = switch (target) { .pane => |t| t, - .top => return null, + .top, .col => return null, }; if (t.file != .pty_run) return null; const run = if (req.handle != 0) openOf(p, req) else for (&p.fs.opens) |*o| { @@ -429,6 +447,19 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr { .mode = f.mode(), .size = topSize(p, f), .mtime = p.fs.started, + .append = f == .tag, + }, + .col => |c| { + if (c.index >= p.ncol) return null; + return .{ + .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.index}) catch unreachable) else "tag", + .node = Node.ofCol(c.index, c.file), + .dir = c.file == .dir, + .mode = if (c.file == .dir) 0o755 else 0o666, + .size = if (c.file == .tag) cols.headerText(p, c.index).?.len + 1 else 0, + .mtime = p.fs.started, + .append = c.file == .tag, + }; }, .pane => |t| { const id = p.paneBySerial(t.serial) orelse return null; @@ -461,7 +492,9 @@ fn topSize(p: *Pardes, f: TopFile) u64 { return switch (f) { // ponytail: /screen has no length until an open renders its frame, // and /pane/new none until an open has a pane to name. - .root, .pane, .screen, .new => 0, + .root, .pane, .col, .screen, .new => 0, + .layout => cols.layoutLen(p), + .tag => cols.headerText(p, null).?.len + 1, .index => pane.indexLen(p), .README => fs.help.len, .status => ctl.statusLen(p), @@ -492,6 +525,7 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply { .pty => Node.of(t.serial, .dir), else => return Reply.fail(req.tag, E.NOTDIR), }, + .col => |c| if (c.file == .dir) @intFromEnum(TopFile.col) else return Reply.fail(req.tag, E.NOTDIR), }; return handle(p, .{ .tag = req.tag, .op = .getattr, .node = parent }); } @@ -514,8 +548,19 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply { if (serial == 0) return Reply.fail(req.tag, E.NOENT); break :pane Node.of(serial, .dir); }, + // A column is its index, in decimal digits. + .col => col: { + if (name.len > 4) return Reply.fail(req.tag, E.NOENT); + for (name) |c| if (c < '0' or c > '9') return Reply.fail(req.tag, E.NOENT); + break :col Node.ofCol(std.fmt.parseInt(u32, name, 10) catch return Reply.fail(req.tag, E.NOENT), .dir); + }, else => return Reply.fail(req.tag, E.NOTDIR), }, + .col => |c| col: { + if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR); + if (!std.mem.eql(u8, name, "tag")) return Reply.fail(req.tag, E.NOENT); + break :col Node.ofCol(c.index, .tag); + }, .pane => |t| pane: { _ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); const f = switch (t.file) { @@ -555,7 +600,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { .root => { // The guide, then what describes the session, then the // files that act, then the directories. - inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| { + inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.pane, TopFile.col }) |t| { if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName()); } if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os"); @@ -575,8 +620,21 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { stageDirent(out, p.gpa, Node.of(serial, .dir), true, name); } }, + .col => for (0..p.ncol) |c| { + if (skip > 0) { + skip -= 1; + continue; + } + var buf: [16]u8 = undefined; + stageDirent(out, p.gpa, Node.ofCol(c, .dir), true, std.fmt.bufPrint(&buf, "{d}", .{c}) catch unreachable); + }, else => return Reply.fail(req.tag, E.NOTDIR), }, + .col => |c| { + if (c.index >= p.ncol) return Reply.fail(req.tag, E.NOENT); + if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR); + if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.index, .tag), false, "tag"); + }, .pane => |t| { const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); const terminal = p.panes[id].?.isTerminal(); @@ -610,6 +668,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { .log => .{ .log = .{} }, else => return .{ .tag = req.tag }, }, + .col => |c| return if (c.index < p.ncol) .{ .tag = req.tag } else Reply.fail(req.tag, E.NOENT), .pane => |t| what: { 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); @@ -665,7 +724,7 @@ fn remove(p: *Pardes, req: Req) Reply { const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT); const t = switch (target) { .pane => |t| t, - .top => return Reply.fail(req.tag, E.PERM), + .top, .col => return Reply.fail(req.tag, E.PERM), }; if (t.file != .dir) return Reply.fail(req.tag, E.PERM); const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); @@ -690,6 +749,8 @@ fn releaseHandle(p: *Pardes, req: Req) void { p.panes[id].?.fs.tag_rewrite = false; p.panes[id].?.fs.tag_held_newline = false; }; + if (t == .col and t.col.file == .tag) cols.released(p, t.col.index); + if (t == .top and t.top == .tag) cols.released(p, null); const o = openOf(p, req) orelse return; const of: ?*Pane = if (t == .pane) (if (p.paneBySerial(t.pane.serial)) |id| p.panes[id] else null) else null; if (of) |pn| switch (o.what) { @@ -725,7 +786,8 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply { return failText(req.tag, E.INVAL, pane.e_addr_failed); if (pane.truncate(p, p.panes[id].?, t.file) != .ok) return Reply.fail(req.tag, E.NOMEM); }, - .top => {}, + .col => |c| if (c.file == .tag and cols.truncate(p, c.index) != .ok) return Reply.fail(req.tag, E.NOENT), + .top => |f| if (f == .tag and cols.truncate(p, null) != .ok) return Reply.fail(req.tag, E.NOMEM), }; return .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) }; } @@ -770,8 +832,9 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { var last: u32 = 0; while (nextSerialAfter(p, last)) |serial| { last = 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 {}; + const id = p.paneBySerial(serial).?; + const pn = p.panes[id].?; + out.print(p.gpa, "{d} {s} {d} {s} {d}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn), pane.columnOf(p, id) }) catch {}; } break :index stagedReply(p, req); }, @@ -783,6 +846,13 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { .focus => ctl.readFocus(p, req), .ctl => ctl.readRoot(p, req), .commands => ctl.readCommands(p, req), + .layout => layout: { + var text: std.Io.Writer.Allocating = .init(p.scratch.allocator()); + cols.writeLayout(p, &text.writer) catch break :layout Reply.fail(req.tag, E.NOMEM); + p.fs.stage(p.gpa).appendSlice(p.gpa, text.written()) catch break :layout Reply.fail(req.tag, E.NOMEM); + break :layout stagedReply(p, req); + }, + .tag => cols.read(p, req, null), // The serial the open handed this fid, so that two reads of one // fid answer the same pane: the read observes, the open acted. .new => serial: { @@ -791,7 +861,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { break :serial Reply.fail(req.tag, E.NOMEM); break :serial stagedReply(p, req); }, - .root, .pane => Reply.fail(req.tag, E.PERM), + .root, .pane, .col => Reply.fail(req.tag, E.PERM), }, .pane => |t| { // A run's answer outlives the pane it ran in. @@ -807,6 +877,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.read(p, req, id, pn, t.file); }, + .col => |c| return if (c.file == .tag) cols.read(p, req, c.index) else Reply.fail(req.tag, E.PERM), } } @@ -817,6 +888,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { .log => events.writeLog(p, req), .focus => ctl.writeFocus(p, req), .ctl => ctl.writeRoot(p, req), + .tag => cols.write(p, req, null), else => Reply.fail(req.tag, E.PERM), }, .pane => |t| { @@ -825,6 +897,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.write(p, req, id, pn, t.file); }, + .col => |c| return if (c.file == .tag) cols.write(p, req, c.index) else Reply.fail(req.tag, E.PERM), } } @@ -836,6 +909,7 @@ test { _ = addr; _ = ctl; _ = pty; + _ = cols; _ = sources; _ = screen; } @@ -968,12 +1042,13 @@ test "readdir lists the root and a pane directory without creating anything" { @memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes); const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf); try testing.expect(top.len >= 10); - for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| { + for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "layout", "tag", "pane", "col", "os" }, 0..) |name, i| { try testing.expectEqualStrings(name, top[i].name); - try testing.expectEqual(i >= 11, top[i].dir); + try testing.expectEqual(i >= 13, top[i].dir); } - try testing.expectEqual(@intFromEnum(TopFile.pane), top[11].node); - try testing.expectEqual(fs.os_root, top[12].node); + try testing.expectEqual(@intFromEnum(TopFile.pane), top[13].node); + try testing.expectEqual(@intFromEnum(TopFile.col), top[14].node); + try testing.expectEqual(fs.os_root, top[15].node); var idbuf: [16]u8 = undefined; try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null); try testing.expect(th.nameAt(top, "self") == null); |
