diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 00:50:45 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 7aa057e29b729ad6d9ac53e3a06f99858fc449e4 (patch) | |
| tree | a0010efb342018f32f98d9e805680306feb88836 /src/ninep/cols.zig | |
| parent | e563be42a5b3e9243e2c01d34cf8e94d2047b753 (diff) | |
| download | pardes-7aa057e29b729ad6d9ac53e3a06f99858fc449e4.tar.gz pardes-7aa057e29b729ad6d9ac53e3a06f99858fc449e4.zip | |
A column has a ctl, an exec and an rmdir over 9P, and Restore maps its serial
A script could read and write a column's tag but not act on the column.
col/<serial>/ctl takes Delcol, Joincol, New and Tty, each acting on that
column as from its tag; col/<serial>/exec runs a word as a middle click in
the tag; rmdir col/<serial> closes an empty column and refuses one with
panes (ENOTEMPTY, an err record saying why). The root ctl's refusal of
Delcol and Joincol points at col/<serial>/ctl. A dump keeps each column's
serial and Restore logs restoredcol <old> <new>; the dump golden gains
them. Documented: Joincol's survivor, the column on the right, keeps its
serial and tag.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/cols.zig')
| -rw-r--r-- | src/ninep/cols.zig | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 79943b1b..b6dc7d26 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -122,6 +122,52 @@ pub fn released(p: *Pardes, serial: ?u32) void { p.fs.header_held = false; } +/// A column's ctl: the words that act on a column, as its tag's clicks do -- +/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag. +pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply { + var lines = std.mem.splitScalar(u8, req.data, '\n'); + while (lines.next()) |raw| { + const line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0) continue; + const word = line[0 .. std.mem.indexOfAny(u8, line, " \t") orelse line.len]; + const takes = for ([_][]const u8{ "Delcol", "Joincol", "New", "Tty" }) |w| { + if (std.mem.eql(u8, w, word)) break true; + } else false; + if (!takes) return tree.failText(req.tag, E.INVAL, "a column's ctl takes Delcol, Joincol, New or Tty"); + const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + p.exec_column = col; + defer p.exec_column = null; + if (pardes.ctlfs.ctl.runBuiltin(p, req, p.active, line)) |refusal| return refusal; + } + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// A column's exec: each line run as a middle click on it in that column's +/// tag, acting on that column. +pub fn writeExec(p: *Pardes, req: Req, serial: u32) Reply { + var lines = std.mem.splitScalar(u8, req.data, '\n'); + while (lines.next()) |raw| { + const line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0) continue; + for (line) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); + const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + p.exec_column = col; + defer p.exec_column = null; + pardes.ctlfs.ctl.run(p, p.active, true, line); + } + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// rmdir of a column closes it when it is empty; one with panes is +/// refused, saying so (close them, or Delcol on its ctl, which asks first). +pub fn remove(p: *Pardes, req: Req, serial: u32, file: tree.ColFile) Reply { + if (file != .dir) return Reply.fail(req.tag, E.PERM); + const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + if (p.col_n[col] > 0) return tree.failText(req.tag, E.NOTEMPTY, "column not empty: close its panes, or write Delcol to its ctl"); + p.removeColumn(col) catch return Reply.fail(req.tag, E.IO); + return .{ .tag = req.tag }; +} + // ---- tests ---- const testing = std.testing; @@ -200,3 +246,39 @@ test "a focus write makes its pane's column the active one: layout says so and p try testing.expectEqual(@as(usize, 0), layout.findPane(p, p.paneBySerial(made).?).?.col); } } + +test "a column's ctl and exec act on it as its tag would, and rmdir closes it only empty" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const first = layout.columnSerial(p, 0); + p.newColumn(p.active); + const empty = layout.columnSerial(p, 1); + // New on the empty column's ctl fills it, wherever the keyboard is. + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .ctl), "New\n").reply.status); + try testing.expectEqual(@as(usize, 1), p.col_n[1]); + try testing.expectEqual(E.INVAL, th.wr(p, Node.ofCol(empty, .ctl), "Exit\n").errno()); + // rmdir refuses a column with a pane, and says why. + const full = th.call(p, .{ .tag = 1, .op = .release, .node = Node.ofCol(empty, .dir), .remove = true }); + try testing.expectEqual(E.NOTEMPTY, full.errno()); + try testing.expect(th.logHas(p, "column not empty")); + // exec runs a word as a click in that column's tag: Delcol closes it. + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .exec), "Delcol\n").reply.status); + try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, empty)); + // An empty column goes on rmdir. + p.newColumn(p.active); + const made = layout.columnSerial(p, 1); + try testing.expectEqual(tree.Status.ok, th.call(p, .{ .tag = 2, .op = .release, .node = Node.ofCol(made, .dir), .remove = true }).reply.status); + try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, made)); + try testing.expectEqual(first, layout.columnSerial(p, 0)); + // Joincol folds a column into the one on its right, which keeps its own + // serial and tag. + p.newColumn(p.active); + const right = layout.columnSerial(p, 1); + _ = th.wr(p, Node.ofCol(right, .tag), " Mine"); + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(first, .ctl), "Joincol\n").reply.status); + try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, first)); + try testing.expectEqual(@as(?usize, 0), layout.columnBySerial(p, right)); + try testing.expect(std.mem.endsWith(u8, headerText(p, right).?, " Mine")); + // The root's ctl points a column word at the column's own. + try testing.expect(std.mem.endsWith(u8, th.wr(p, @intFromEnum(tree.TopFile.ctl), "Delcol\n").reply.ename, "write it to col/<serial>/ctl")); +} |
