summaryrefslogtreecommitdiff
path: root/src/ninep/cols.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/cols.zig')
-rw-r--r--src/ninep/cols.zig284
1 files changed, 284 insertions, 0 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
new file mode 100644
index 00000000..b6dc7d26
--- /dev/null
+++ b/src/ninep/cols.zig
@@ -0,0 +1,284 @@
+//! The layout and the header tags: /layout, /tag (the workspace's) and
+//! /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");
+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 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>`: the active column, where pane/new and a look
+/// place a pane next (acme's activecol), not always the keyboard's.
+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} {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 (p.activeColumn() orelse active) |c| try w.print("active {d}\n", .{layout.columnSerial(p, c)}) else try w.writeAll("active -\n");
+}
+
+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();
+}
+
+/// 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, 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, 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);
+ 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, 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, serial).?;
+ var data = req.data;
+ 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;
+ 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, 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(serial);
+ p.fs.header_held = false;
+ return .ok;
+}
+
+/// 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, serial: ?u32) void {
+ if (p.fs.header_rewrite != rewriteKey(serial)) return;
+ p.fs.header_rewrite = null;
+ 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;
+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 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, "{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, 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, 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);
+ 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 });
+ _ = 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));
+}
+
+test "a focus write makes its pane's column the active one: layout says so and pane/new goes there" {
+ for ([_][]const u8{ "Placement acme", "Placement pardes" }) |placement| {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ try testing.expect(p.executeBuiltinLine(p.active, placement));
+ const left = p.panes[0].?.serial;
+ p.newColumn(p.active);
+ p.newScratchFor(p.active, .fs); // fills the new, right column
+ p.sync();
+ try testing.expectEqual(@as(usize, 1), layout.findPane(p, p.active).?.col);
+ var digits: [16]u8 = undefined;
+ _ = th.wr(p, @intFromEnum(tree.TopFile.focus), try std.fmt.bufPrint(&digits, "{d}\n", .{left}));
+ var want: [32]u8 = undefined;
+ const text = th.rd(p, @intFromEnum(tree.TopFile.layout), 0, 4096).bytes;
+ try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "active {d}\n", .{layout.columnSerial(p, 0)})));
+ const made = th.call(p, .{ .tag = 1, .op = .open, .node = @intFromEnum(tree.TopFile.new) }).reply.handle;
+ 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"));
+}