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.zig124
1 files changed, 115 insertions, 9 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index b6dc7d26..b00ecdba 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -123,8 +123,11 @@ pub fn released(p: *Pardes, serial: ?u32) void {
}
/// 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.
+/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag. What
+/// they say is the session's in the log (`msg -`), not the focused pane's.
pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
+ p.fs.session_write = true;
+ defer p.fs.session_write = false;
var lines = std.mem.splitScalar(u8, req.data, '\n');
while (lines.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
@@ -133,31 +136,59 @@ pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
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");
+ // Plan 9's words for it, so a mount's errno is EINVAL as the root's
+ // and a pane's ctl give (9ns reads "control message").
+ if (!takes) return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "unknown control message \"{s}\": a column's ctl takes Delcol, Joincol, New or Tty", .{line[0..@min(line.len, 64)]}) catch "unknown control message");
+ // Only Tty takes a word (its shell); `New extra` is no New.
+ if (!std.mem.eql(u8, word, "Tty") and word.len != line.len)
+ return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "wrong #args in control message \"{s}\"", .{line[0..@min(line.len, 64)]}) catch "wrong #args in control message");
const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
p.exec_column = col;
defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
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 {
+/// A column's exec (`serial`), or the workspace tag's (/tagexec, null):
+/// each line run as a middle click on it in that tag, a column's acting on
+/// that column. A read answers the panes the last write touched, as /exec's
+/// does; what the words say is the session's in the log (`msg -`).
+pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply {
+ p.fs.results_len = 0;
+ p.fs.session_write = true;
+ defer p.fs.session_write = false;
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);
+ for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, pardes.ctlfs.ctl.e_control);
+ if (pardes.ctlfs.ctl.tooLong(req, line)) |refusal| return refusal;
+ const col = if (serial) |s| layout.columnBySerial(p, s) orelse return Reply.fail(req.tag, E.NOENT) else null;
+ if (p.panes[p.active] == null) 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);
+ p.exec_header = true;
+ defer p.exec_header = false;
+ if (pardes.ctlfs.ctl.captured(p, req, p.active, true, line, true)) |refusal| return refusal;
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
+test "New written to a column's exec makes its scratch in the session's directory, as tagexec's does" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const session = "/project/src";
+ @memcpy(p.session_dir[0..session.len], session);
+ p.session_dir_len = session.len;
+ const serial = layout.columnSerial(p, 0);
+ const exec_node = tree.Node.ofCol(serial, .exec);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, exec_node, "New\n").reply.status);
+ try testing.expectEqualStrings("/project/src/+New", p.panes[p.active].?.file.?.path);
+}
+
/// 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 {
@@ -257,10 +288,12 @@ test "a column's ctl and exec act on it as its tag would, and rmdir closes it on
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());
+ var rec: [64]u8 = undefined;
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "err - col/{d}/ctl: unknown control message \"Exit\"", .{empty})));
// 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"));
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "err - col/{d}: column not empty", .{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));
@@ -282,3 +315,76 @@ test "a column's ctl and exec act on it as its tag would, and rmdir closes it on
// 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"));
}
+
+test "tagexec runs a word as a click in the workspace tag; exec files read back what they touched; their messages are the session's" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const tagexec = @intFromEnum(tree.TopFile.tagexec);
+ try testing.expectEqual(tree.Status.ok, th.look_up(p, tree.root, "tagexec").reply.status);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Newcol\n").reply.status);
+ try testing.expectEqual(@as(usize, 2), p.ncol);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Msg from the top\n").reply.status);
+ try testing.expect(th.logHas(p, "msg - from the top\n"));
+ // A column's exec makes a pane there, reads it back, and its Msg is
+ // the session's too.
+ const right = layout.columnSerial(p, 1);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(right, .exec), "New\n").reply.status);
+ const made = p.panes[p.col_panes[1][0]].?.serial;
+ var want: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), th.rd(p, Node.ofCol(right, .exec), 0, 64).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), th.rd(p, tagexec, 0, 64).bytes);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(right, .exec), "Msg from a column\n").reply.status);
+ try testing.expect(th.logHas(p, "msg - from a column\n"));
+}
+
+test "a word run from the workspace's or a column's tag runs in the session's directory, a pane's in its own" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const session = p.sessionDir();
+ try testing.expect(!std.mem.eql(u8, session, pardes.Pardes.paneDir(p.panes[0].?))); // the file is elsewhere
+ const dirs = [_]u64{ @intFromEnum(tree.TopFile.tagexec), Node.ofCol(layout.columnSerial(p, 0), .exec) };
+ for (dirs) |node| {
+ try testing.expectEqual(tree.Status.ok, th.wr(p, node, "true\n").reply.status);
+ const made = p.paneBySerial(p.fs.results[0]).?;
+ try testing.expectEqualStrings(session, pardes.Pardes.paneDir(p.panes[made].?));
+ try p.removePane(made, null);
+ }
+ const from_pane = p.panes[0].?.serial;
+ _ = th.wr(p, Node.of(from_pane, .exec), "true\n");
+ const made = p.paneBySerial(p.fs.results[0]).?;
+ try testing.expectEqualStrings(pardes.Pardes.paneDir(p.panes[0].?), pardes.Pardes.paneDir(p.panes[made].?));
+}
+
+test "a command run from a column's tag gets its pane in that column, not the last" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ p.newColumn(p.active);
+ p.newScratchFor(p.active, .fs); // fills the new, right column
+ p.sync();
+ const left = layout.columnSerial(p, 0);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(left, .exec), "true\n").reply.status);
+ const made = p.paneBySerial(p.fs.results[0]).?;
+ try testing.expectEqual(@as(usize, 0), layout.findPane(p, made).?.col);
+ // Finished, it is reused only by its own column's commands.
+ p.panes[made].?.command_done = true;
+ p.panes[made].?.command_pty = false;
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(layout.columnSerial(p, 1), .exec), "true\n").reply.status);
+ const other = p.paneBySerial(p.fs.results[0]).?;
+ try testing.expect(other != made);
+ try testing.expectEqual(@as(usize, 1), layout.findPane(p, other).?.col);
+}
+
+test "a column's ctl refuses an unknown word and an argument as the other ctls do" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const ctl_node = Node.ofCol(layout.columnSerial(p, 0), .ctl);
+ const unknown = th.wr(p, ctl_node, "Bogus\n");
+ try testing.expectEqual(E.INVAL, unknown.errno());
+ try testing.expect(std.mem.startsWith(u8, unknown.reply.ename, "unknown control message \"Bogus\""));
+ const extra = th.wr(p, ctl_node, "New extra\n");
+ try testing.expectEqual(E.INVAL, extra.errno());
+ try testing.expect(std.mem.startsWith(u8, extra.reply.ename, "wrong #args"));
+ try testing.expectEqual(@as(usize, 1), p.col_n[0]);
+ // A pane's bare answer and a winsize of no size say what they take.
+ try testing.expectEqualStrings("answer takes a choice or -", th.wr(p, Node.of(p.panes[0].?.serial, .ctl), "answer\n").reply.ename);
+}