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/cols.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/cols.zig')
| -rw-r--r-- | src/ninep/cols.zig | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig new file mode 100644 index 00000000..e1688517 --- /dev/null +++ b/src/ninep/cols.zig @@ -0,0 +1,163 @@ +//! The layout and the header tags: /layout, /tag (the workspace's) and +//! /col/<n>/tag (each column's). acme serves no column files -- its columns +//! are only a window's place -- so these go past it: a script can see where +//! the panes are and edit the tags a person clicks in. +const std = @import("std"); +const pardes = @import("../pardes.zig"); +const Pardes = pardes.Pardes; +const tree = @import("tree.zig"); +const tagline = pardes.tagline; +const layout = pardes.layout; +const Text = pardes.panes.Text; +const Req = tree.Req; +const Reply = tree.Reply; +const E = tree.E; + +/// The column with the keyboard: the header's, when a column tag has it, +/// else the active pane's. Null when the workspace tag has it and no pane +/// is active. +pub fn activeColumn(p: *const Pardes) ?usize { + if (p.header_focus) if (p.header_column) |c| return if (c < p.ncol) c else null; + const at = layout.findPane(p, p.active) orelse return null; + return at.col; +} + +/// One line a column: its index, x and width in cells, `current` or +/// `notcurrent` (rio's words, for the column with the keyboard), `empty` or +/// `full`, and its panes' serials, top to bottom; then `active <index>`. +pub fn writeLayout(p: *const Pardes, w: *std.Io.Writer) !void { + const active = activeColumn(p); + for (0..p.ncol) |c| { + try w.print("{d} {d} {d} {s} {s}", .{ + c, p.col_x[c], p.col_w[c], + if (active == c) "current" else "notcurrent", if (p.col_n[c] == 0) "empty" else "full", + }); + for (p.col_panes[c][0..p.col_n[c]]) |id| if (p.panes[id]) |pane| try w.print(" {d}", .{pane.serial}); + try w.writeByte('\n'); + } + if (active) |c| try w.print("active {d}\n", .{c}) else try w.writeAll("active -\n"); +} + +pub fn layoutLen(p: *const Pardes) u64 { + var buf: [256]u8 = undefined; + var counting: std.Io.Writer.Discarding = .init(&buf); + writeLayout(p, &counting.writer) catch return 0; + return counting.fullCount(); +} + +/// The header a node names: the workspace's (`col` null) or a column's. +fn header(p: *Pardes, col: ?usize) ?*Text { + const c = col orelse return &p.global_tag; + return if (c < p.ncol) &p.col_tags[c] else null; +} + +pub fn headerText(p: *Pardes, col: ?usize) ?[]const u8 { + const c = col orelse return tagline.topbar(p); + return if (c < p.ncol) tagline.columnTag(p, c) else null; +} + +pub fn read(p: *Pardes, req: Req, col: ?usize) Reply { + const text = headerText(p, col) orelse return Reply.fail(req.tag, E.NOENT); + const out = p.fs.stage(p.gpa); + out.appendSlice(p.gpa, text) catch return Reply.fail(req.tag, E.NOMEM); + out.append(p.gpa, '\n') catch return Reply.fail(req.tag, E.NOMEM); + return tree.stagedReply(p, req); +} + +/// A write appends, as a pane tag's does; the tag is one line, so a newline +/// in it becomes a space (tagline.flattenHeaders), but after a truncation +/// the newline that ends the write is dropped, as a pane tag drops it. +pub fn write(p: *Pardes, req: Req, col: ?usize) Reply { + const t = header(p, col) orelse return Reply.fail(req.tag, E.NOENT); + if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; + const was = headerText(p, col).?; + var data = req.data; + const rewriting = p.fs.header_rewrite == rewriteKey(col); + // A newline held from the write before goes in once more text follows. + const held: []const u8 = if (rewriting and p.fs.header_held) " " else ""; + p.fs.header_held = false; + if (rewriting and data[data.len - 1] == '\n') { + data = data[0 .. data.len - 1]; + p.fs.header_held = true; + } + const next = std.mem.concat(p.gpa, u8, &.{ was, held, data }) catch return Reply.fail(req.tag, E.NOMEM); + std.mem.replaceScalar(u8, next, '\n', ' '); + if (t.own) |own| p.gpa.free(own); + t.own = next; + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// Truncating clears it, as a pane tag's `cleartag`: its default words go +/// too, and `u` in it brings them back. +pub fn truncate(p: *Pardes, col: ?usize) tree.Status { + const t = header(p, col) orelse return .err; + const empty = p.gpa.alloc(u8, 0) catch return .err; + t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null); + if (t.own) |own| p.gpa.free(own); + t.own = empty; + t.cur_row = 0; + t.cur_col = 0; + p.fs.header_rewrite = rewriteKey(col); + p.fs.header_held = false; + return .ok; +} + +/// Which header a truncation was of: the workspace's is 0, a column's its +/// index + 1. +fn rewriteKey(col: ?usize) u32 { + return if (col) |c| @intCast(c + 1) else 0; +} + +pub fn released(p: *Pardes, col: ?usize) void { + if (p.fs.header_rewrite != rewriteKey(col)) return; + p.fs.header_rewrite = null; + p.fs.header_held = false; +} + +// ---- tests ---- + +const testing = std.testing; +const th = @import("testing.zig"); +const Node = tree.Node; + +test "layout lists each column's place, panes and state, and the active one" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + p.newColumn(p.active); + p.sync(); + const layout_node = @intFromEnum(tree.TopFile.layout); + const text = th.rd(p, layout_node, 0, 4096).bytes; + var want: [256]u8 = undefined; + const first = try std.fmt.bufPrint(&want, "0 {d} {d} notcurrent full {d}\n", .{ p.col_x[0], p.col_w[0], p.panes[0].?.serial }); + try testing.expect(std.mem.startsWith(u8, text, first)); + // Newcol gives the new, empty column's tag the keyboard. + try testing.expect(std.mem.indexOf(u8, text, "current empty\n") != null); + try testing.expect(std.mem.endsWith(u8, text, "active 1\n")); + try testing.expectEqual(@as(u64, text.len), th.call(p, .{ .tag = 1, .op = .getattr, .node = layout_node }).reply.attr.size); + // /index says which column each pane is in, last. + try testing.expect(std.mem.endsWith(u8, th.rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, " 0\n")); + try testing.expect(th.logHas(p, "newcol 1\n")); +} + +test "a column's tag and the workspace's read, replace and append as a pane tag does, on one line" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const col_dir = @intFromEnum(tree.TopFile.col); + try testing.expectEqual(tree.Status.ok, th.look_up(p, col_dir, "0").reply.status); + try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "7").errno()); + const tag = Node.ofCol(0, .tag); + try testing.expectEqual(tree.Status.ok, th.look_up(p, Node.ofCol(0, .dir), "tag").reply.status); + try testing.expectEqualStrings("New Tty Find Grep Joincol Delcol\n", th.rd(p, tag, 0, 4096).bytes); + // `echo Make > tag`: cleared, then the newline ending the write dropped. + _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true }); + _ = th.wr(p, tag, "Make\n"); + try testing.expectEqualStrings("Make\n", th.rd(p, tag, 0, 4096).bytes); + _ = th.call(p, .{ .tag = 2, .op = .release, .node = tag }); + // `>>` appends; a newline in a one-line tag is a space. + _ = th.wr(p, tag, " Lint\nNow"); + try testing.expectEqualStrings("Make Lint Now\n", th.rd(p, tag, 0, 4096).bytes); + const top = @intFromEnum(tree.TopFile.tag); + _ = th.call(p, .{ .tag = 3, .op = .setattr, .node = top, .truncate = true }); + _ = th.wr(p, top, "Newcol Exit\n"); + try testing.expectEqualStrings("Newcol Exit", tagline.topbar(p)); +} |
