summaryrefslogtreecommitdiff
path: root/src/ninep/cols.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 00:23:55 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit4881c5d13c6a705a8c8bdb0181c33f9a500b5a74 (patch)
tree31ee1398657b271d08830a1efad9e6d9f206aa68 /src/ninep/cols.zig
parent15ddd805905f785f4dde463d4d6b91e1d2c41e26 (diff)
downloadpardes-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.zig98
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 });