diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 00:23:55 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 4881c5d13c6a705a8c8bdb0181c33f9a500b5a74 (patch) | |
| tree | 31ee1398657b271d08830a1efad9e6d9f206aa68 /src/ninep/cols.zig | |
| parent | 15ddd805905f785f4dde463d4d6b91e1d2c41e26 (diff) | |
| download | pardes-4881c5d13c6a705a8c8bdb0181c33f9a500b5a74.tar.gz pardes-4881c5d13c6a705a8c8bdb0181c33f9a500b5a74.zip | |
Columns are named by stable serials over 9P, as panes are
/col/<n> named a column by its index, which shifts when a column left of it
closes: exactly what scripts trip on. A column is now /col/<serial>, its
serial kept while it lives and never reused; /layout lines give serial and
index (`<serial> <index> ...`), active names the serial, /index's last
field is the column's serial, and the log says newcol and delcol with
serials. Docs, the skill and the tree's README 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 | 98 |
1 files changed, 58 insertions, 40 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index e1688517..682c2a02 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -1,5 +1,6 @@ //! 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 +//! /col/<serial>/tag (each column's; a column's serial stays while it lives, +//! as a pane's does). 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"); @@ -22,42 +23,48 @@ pub fn activeColumn(p: *const Pardes) ?usize { 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 { +/// One line a column: its serial (the name /col/<serial> and the log give +/// it, which stays while it lives), its index left to right, 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 <serial>`. +pub fn writeLayout(p: *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], + try w.print("{d} {d} {d} {d} {s} {s}", .{ + layout.columnSerial(p, c), 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"); + if (active) |c| try w.print("active {d}\n", .{layout.columnSerial(p, c)}) else try w.writeAll("active -\n"); } -pub fn layoutLen(p: *const Pardes) u64 { +pub fn layoutLen(p: *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; +/// A header by its node: the workspace's (`serial` null) or the column's +/// with that serial; null when that column has closed. +const Header = struct { text: *Text, col: ?usize }; + +fn header(p: *Pardes, serial: ?u32) ?Header { + const s = serial orelse return .{ .text = &p.global_tag, .col = null }; + const c = layout.columnBySerial(p, s) orelse return null; + return .{ .text = &p.col_tags[c], .col = c }; } -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 headerText(p: *Pardes, serial: ?u32) ?[]const u8 { + const h = header(p, serial) orelse return null; + return if (h.col) |c| tagline.columnTag(p, c) else tagline.topbar(p); } -pub fn read(p: *Pardes, req: Req, col: ?usize) Reply { - const text = headerText(p, col) orelse return Reply.fail(req.tag, E.NOENT); +pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply { + const text = headerText(p, serial) 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); @@ -67,12 +74,12 @@ pub fn read(p: *Pardes, req: Req, col: ?usize) Reply { /// 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); +pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply { + const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text; if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; - const was = headerText(p, col).?; + const was = headerText(p, serial).?; var data = req.data; - const rewriting = p.fs.header_rewrite == rewriteKey(col); + const rewriting = p.fs.header_rewrite == rewriteKey(serial); // 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; @@ -89,27 +96,27 @@ pub fn write(p: *Pardes, req: Req, col: ?usize) Reply { /// 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; +pub fn truncate(p: *Pardes, serial: ?u32) tree.Status { + const t = (header(p, serial) orelse return .err).text; 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_rewrite = rewriteKey(serial); 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; +/// Which header a truncation was of: the workspace's is 0 (no column's +/// serial is), a column's its serial. +fn rewriteKey(serial: ?u32) u32 { + return serial orelse 0; } -pub fn released(p: *Pardes, col: ?usize) void { - if (p.fs.header_rewrite != rewriteKey(col)) return; +pub fn released(p: *Pardes, serial: ?u32) void { + if (p.fs.header_rewrite != rewriteKey(serial)) return; p.fs.header_rewrite = null; p.fs.header_held = false; } @@ -125,28 +132,39 @@ test "layout lists each column's place, panes and state, and the active one" { defer p.deinit(); p.newColumn(p.active); p.sync(); + const first_serial = layout.columnSerial(p, 0); + const new_serial = layout.columnSerial(p, 1); + try testing.expect(first_serial != new_serial); 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 }); + const first = try std.fmt.bufPrint(&want, "{d} 0 {d} {d} notcurrent full {d}\n", .{ first_serial, 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.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "active {d}\n", .{new_serial}))); 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")); + // /index says which column each pane is in, by its serial, last. + try testing.expect(std.mem.endsWith(u8, th.rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, try std.fmt.bufPrint(&want, " {d}\n", .{first_serial}))); + try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "newcol {d}\n", .{new_serial}))); + // A column that closes takes its name with it; the others keep theirs. + try p.removeColumn(1); + try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "delcol {d}\n", .{new_serial}))); + try testing.expectEqual(E.NOENT, th.look_up(p, @intFromEnum(tree.TopFile.col), try std.fmt.bufPrint(&want, "{d}", .{new_serial})).errno()); + try testing.expectEqual(first_serial, layout.columnSerial(p, 0)); } 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); + const serial = layout.columnSerial(p, 0); + var name: [16]u8 = undefined; + try testing.expectEqual(tree.Status.ok, th.look_up(p, col_dir, try std.fmt.bufPrint(&name, "{d}", .{serial})).reply.status); + try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "0").errno()); + try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "777").errno()); + const tag = Node.ofCol(serial, .tag); + try testing.expectEqual(tree.Status.ok, th.look_up(p, Node.ofCol(serial, .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 }); |
