summaryrefslogtreecommitdiff
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
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]>
-rw-r--r--.agents/skills/pardes-9p/SKILL.md6
-rw-r--r--README.md2
-rw-r--r--docs/fs.md23
-rw-r--r--src/fs-help.txt6
-rw-r--r--src/layout.zig13
-rw-r--r--src/ninep/cols.zig98
-rw-r--r--src/ninep/events.zig2
-rw-r--r--src/ninep/pane.zig15
-rw-r--r--src/ninep/tree.zig49
9 files changed, 120 insertions, 94 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index a5447991..8c383962 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -52,7 +52,7 @@ match this document, that session predates the change; restart it.
```
$m/README the served guide, worth reading first
-$m/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column
+$m/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial
$m/status pid, version, panes
$m/look write a line = a right click on it at the active pane (file:/re/, file:#n, :/re/
select by address, as acme's look, from the file's dot: file:0/re/ for the
@@ -82,8 +82,8 @@ $m/ctl the settings, one a line as a write takes them; write a setting or
a pane's builtins (Del, Save f, Collapse, which folds that pane, and the
column word Delcol, which closes that pane's column) go to $m/pane/<n>/ctl
$m/commands every builtin: `Word`, `Word arg`, then `root` or `pane` (which ctl takes it)
-$m/layout one line per column: index x width current|notcurrent empty|full serials...; active <index>
-$m/tag the workspace tag (> replaces, >> appends, one line); $m/col/<n>/tag a column's, n from 0
+$m/layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; active <serial>
+$m/tag the workspace tag (> replaces, >> appends, one line); $m/col/<serial>/tag a column's (serials stay, as panes' do)
$m/pane/new open it to make a pane (a scratch named <dir>/+New), read names it; it goes in
the ACTIVE column (the one last typed or clicked in, or Newcol's), filling it if
empty, else taking the bottom half of its last pane (ctl `Placement pardes`: the old rules);
diff --git a/README.md b/README.md
index e487bf9c..72cfcbb0 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ mounted through kernel v9fs. It asks sudo in that pane, then starts your shell
as your normal user. Access the tree through `$PARDES_MOUNT`.
See [mounted terminals](docs/v9fs.md). Beyond acme's per-window files the tree
serves the layout: `/layout` lists the columns, their places and panes, and
-`/tag` and `/col/<n>/tag` are the workspace and column tags, editable like a
+`/tag` and `/col/<serial>/tag` are the workspace and column tags, editable like a
window's ([the tree](docs/fs.md#the-served-tree)).
## Requirements
diff --git a/docs/fs.md b/docs/fs.md
index 5f382e7e..fd18d523 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -92,7 +92,7 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
```
/README this guide, also src/fs-help.txt
-/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column
+/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial
/status pid, version and pane count
/look write a line: a right click on it at the active pane; read: the serials the last
look, exec or ctl write touched (made, else acted at)
@@ -105,10 +105,10 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
/focus the serial of the pane with the keyboard; write a serial to give it the keyboard
/ctl the settings, one a line as a write takes them; write a setting or a session builtin
/commands every builtin: word, `arg` if it takes one, `root` or `pane` (the ctl that takes it), a setting's values
-/layout one line per column: index x width current|notcurrent empty|full serials...;
- then active <index> (- when no column has the keyboard)
+/layout one line per column, left to right: serial index x width current|notcurrent
+ empty|full pane-serials...; then active <serial> (- when no column has the keyboard)
/tag the workspace tag; > replaces it, >> appends, one line
-/col/<n>/tag column n's tag, the same way; columns count from 0, left to right
+/col/<n>/tag the tag of the column with serial n, the same way
/pane/new open it to make a pane; the read answers that pane's serial
/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll
errors event look exec, plus pty/{ctl,status,data} on terminals
@@ -118,13 +118,14 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
`/layout`, `/tag` and `/col` go past acme, which serves no column files --
its columns are only where a window sits. They are here so a script can see
-where the panes are (`/index`'s last word is each one's column, the same
-index) and edit the tags a person clicks in: a column tag is one line, a
-newline written into it a space, and a truncating write (`echo Make >
-col/0/tag`) clears it and drops the newline that ends it, as a pane tag's
-does. A column is named by its index, so the columns right of one that
-closes move down one; the log says `newcol <n>` and `delcol <n>` as they come
-and go, with /layout to read again.
+where the panes are (`/index`'s last word is each one's column serial) and
+edit the tags a person clicks in: a column tag is one line, a newline
+written into it a space, and a truncating write (`echo Make > col/3/tag`)
+clears it and drops the newline that ends it, as a pane tag's does. A
+column is named by its serial, as a pane is: it stays while the column
+lives, whatever opens or closes beside it, and is never reused; /layout
+gives each column's serial and its index left to right, and the log says
+`newcol <serial>` and `delcol <serial>` as columns come and go.
Control messages are split by what they act on, as acme keeps window verbs
on a window's ctl and webfs and upas/fs keep session settings on a root ctl.
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 2f944c8e..50dc5d9c 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -1,7 +1,7 @@
Pardes control filesystem
=========================
README this guide
-index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name (+New if unnamed), column
+index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name (+New if unnamed), column serial
status pid, version and pane count
look write a line: a right click on it at the active pane; read: the serials it touched
exec write a line: a middle click, an editor command word or a shell line; read the same
@@ -11,9 +11,9 @@ listeners the session's dial addresses
focus the serial of the pane with the keyboard (empty on a column tag); write a serial to give it the keyboard
ctl settings, one a line; or Newcol (an empty column), Dump, Exit (quits, asks once), Kill [w] (stops what pardes ran, w: by first word)
commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it
-layout one line per column: index x width current|notcurrent empty|full serials...; then active <index>
+layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; then active <serial>
tag the workspace tag: read it, > replaces it, >> appends (one line)
-col/<n>/tag a column's tag, the same way (columns count from 0, as in layout)
+col/<n>/tag the tag of the column with serial n, the same way (a serial stays while the column lives)
pane/new open it to make a pane (named +New) in the active column, filling it if empty; the read answers its serial
pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only:
appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes
diff --git a/src/layout.zig b/src/layout.zig
index 25c95d30..b9b24e79 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -322,6 +322,7 @@ pub fn removePane(p: *Pardes, id: usize) void {
/// it (the first column's to its right), as acme's rowclose gives it.
pub fn dropColumn(p: *Pardes, c: usize) void {
std.debug.assert(p.col_n[c] == 0);
+ const serial = columnSerial(p, c);
tagline.exitHeader(p);
if (p.ncol > 1) p.col_weight[if (c > 0) c - 1 else c + 1] +|= p.col_weight[c];
p.col_tags[c].deinit(p.gpa);
@@ -336,14 +337,14 @@ pub fn dropColumn(p: *Pardes, c: usize) void {
p.ncol -= 1;
p.col_tags[p.ncol] = .{ .what = .column, .gpa = p.gpa };
p.col_serial[p.ncol] = 0;
- noteColumn(p, "delcol", c);
+ noteColumn(p, "delcol", serial);
}
-/// `newcol <n>` or `delcol <n>` in the log: the index, as /layout and
-/// /col number columns, the ones after it moving up or down one.
-fn noteColumn(p: *Pardes, what: []const u8, c: usize) void {
+/// `newcol <serial>` or `delcol <serial>` in the log: the column's serial,
+/// as /layout and /col name it.
+fn noteColumn(p: *Pardes, what: []const u8, serial: u32) void {
var buf: [16]u8 = undefined;
- pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d}", .{c}) catch return);
+ pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d}", .{serial}) catch return);
}
/// Fold column `c` into the one on its right, its panes below theirs.
@@ -499,7 +500,7 @@ pub fn insertColumn(p: *Pardes, col: usize, before: bool) ?usize {
p.col_serial[c] = 0;
p.col_n[c] = 0;
p.ncol += 1;
- noteColumn(p, "newcol", c);
+ noteColumn(p, "newcol", columnSerial(p, c));
return c;
}
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 });
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index fc410d5a..e918f09a 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -174,7 +174,7 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
.top => |f| file = f.fileName(),
.col => |c| {
serial = "-";
- file = std.fmt.bufPrint(&name, "col/{d}/tag", .{c.index}) catch "col";
+ file = std.fmt.bufPrint(&name, "col/{d}/tag", .{c.serial}) catch "col";
},
.pane => |pt| {
serial = std.fmt.bufPrint(&name, "{d}", .{pt.serial}) catch "-";
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 6542d747..c594c7a3 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -265,10 +265,11 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 {
// ---- the index ----
-/// The column a pane is in, as /layout and /col number them; the last word
-/// of its /index line.
-pub fn columnOf(p: *const Pardes, id: usize) usize {
- return if (pardes.layout.findPane(p, id)) |at| at.col else 0;
+/// The serial of the column a pane is in, as /layout and /col name it; the
+/// last word of its /index line.
+pub fn columnOf(p: *Pardes, id: usize) u32 {
+ const at = pardes.layout.findPane(p, id) orelse return 0;
+ return pardes.layout.columnSerial(p, at.col);
}
pub fn indexLen(p: *Pardes) u64 {
@@ -710,7 +711,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
const a = rd(p, index, 0, 4096);
try testing.expectEqual(Status.ok, a.reply.status);
var want: [64]u8 = undefined;
- const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt 0\n", .{pane.serial});
+ const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt {d}\n", .{ pane.serial, columnOf(p, 0) });
try testing.expect(std.mem.indexOf(u8, a.bytes, clean_row) != null);
const at = std.mem.indexOf(u8, a.bytes, clean_row).?;
const stat = call(p, .{ .tag = 1, .op = .getattr, .node = index });
@@ -719,7 +720,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
pane.file.?.saved_revision = pane.file.?.revision -% 1;
var dirty_want: [64]u8 = undefined;
- const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt 0\n", .{pane.serial});
+ const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt {d}\n", .{ pane.serial, columnOf(p, 0) });
try testing.expect(std.mem.indexOf(u8, rd(p, index, 0, 4096).bytes, dirty_row) != null);
const kind = at + std.mem.indexOfScalar(u8, clean_row, ' ').? + 1;
try testing.expectEqualStrings("text", rd(p, index, kind, 4).bytes);
@@ -741,7 +742,7 @@ test "a terminal is listed as term with its directory as name" {
try testing.expect(std.mem.indexOf(
u8,
rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes,
- try std.fmt.bufPrint(&want, "{d} term 0 /work/dir 0\n", .{pane.serial}),
+ try std.fmt.bufPrint(&want, "{d} term 0 /work/dir {d}\n", .{ pane.serial, columnOf(p, 0) }),
) != null);
try testing.expectEqualStrings("/work/dir\n", rd(p, Node.of(pane.serial, .name), 0, 4096).bytes);
const refused = wr(p, Node.of(pane.serial, .name), "/elsewhere\n");
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 9459be4f..3be75960 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -19,6 +19,7 @@ pub const pane = @import("pane.zig");
pub const addr = @import("addr.zig");
pub const ctl = @import("ctl.zig");
pub const cols = @import("cols.zig");
+const layout = pardes.layout;
pub const pty = @import("pty.zig");
pub const sources = @import("sources.zig");
pub const screen = @import("screen.zig");
@@ -268,8 +269,9 @@ 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).
+/// /col/<serial> and its tag: a column is named by its serial, which stays
+/// while it lives as a pane's does; this bit, past a pane serial's 32, keeps
+/// the two apart.
pub const ColFile = enum(u5) { dir = 0, tag = 1 };
const col_bit: u59 = 1 << 58;
@@ -282,8 +284,8 @@ 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 ofCol(serial: u32, file: ColFile) u64 {
+ return @bitCast(Node{ .file = @intFromEnum(file), .serial = col_bit | @as(u59, serial) });
}
pub fn target(node: u64) ?Target {
@@ -292,7 +294,7 @@ pub const Node = packed struct(u64) {
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,
+ .serial = std.math.cast(u32, n.serial & ~col_bit) orelse return null,
.file = std.enums.fromInt(ColFile, n.file) orelse return null,
} };
return .{ .pane = .{
@@ -305,7 +307,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 },
+ col: struct { serial: u32, file: ColFile },
};
/// Opening /pane/new makes a pane and reading the open fid answers its
@@ -450,13 +452,13 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
.append = f == .tag,
},
.col => |c| {
- if (c.index >= p.ncol) return null;
+ if (layout.columnBySerial(p, c.serial) == null) 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),
+ .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else "tag",
+ .node = Node.ofCol(c.serial, 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,
+ .size = if (c.file == .tag) cols.headerText(p, c.serial).?.len + 1 else 0,
.mtime = p.fs.started,
.append = c.file == .tag,
};
@@ -548,18 +550,20 @@ 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.
+ // A column is its serial, in decimal digits and never 0.
.col => col: {
- if (name.len > 4) return Reply.fail(req.tag, E.NOENT);
+ 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);
- break :col Node.ofCol(std.fmt.parseInt(u32, name, 10) catch return Reply.fail(req.tag, E.NOENT), .dir);
+ 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 :col Node.ofCol(serial, .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);
+ break :col Node.ofCol(c.serial, .tag);
},
.pane => |t| pane: {
_ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
@@ -626,14 +630,15 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
continue;
}
var buf: [16]u8 = undefined;
- stageDirent(out, p.gpa, Node.ofCol(c, .dir), true, std.fmt.bufPrint(&buf, "{d}", .{c}) catch unreachable);
+ const serial = layout.columnSerial(p, c);
+ stageDirent(out, p.gpa, Node.ofCol(serial, .dir), true, std.fmt.bufPrint(&buf, "{d}", .{serial}) 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 (layout.columnBySerial(p, c.serial) == null) 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");
+ if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, .tag), false, "tag");
},
.pane => |t| {
const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
@@ -668,7 +673,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),
+ .col => |c| return if (layout.columnBySerial(p, c.serial) != null) .{ .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);
@@ -749,7 +754,7 @@ 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 == .col and t.col.file == .tag) cols.released(p, t.col.serial);
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;
@@ -786,7 +791,7 @@ 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);
},
- .col => |c| if (c.file == .tag and cols.truncate(p, c.index) != .ok) return Reply.fail(req.tag, E.NOENT),
+ .col => |c| if (c.file == .tag and cols.truncate(p, c.serial) != .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) };
@@ -877,7 +882,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),
+ .col => |c| return if (c.file == .tag) cols.read(p, req, c.serial) else Reply.fail(req.tag, E.PERM),
}
}
@@ -897,7 +902,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),
+ .col => |c| return if (c.file == .tag) cols.write(p, req, c.serial) else Reply.fail(req.tag, E.PERM),
}
}