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.zig128
1 files changed, 122 insertions, 6 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index cc583c93..454cce4a 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -7,6 +7,7 @@ const std = @import("std");
const pardes = @import("../pardes.zig");
const Pardes = pardes.Pardes;
const tree = @import("tree.zig");
+const limits = @import("../memory.zig").limits;
const tagline = pardes.tagline;
const layout = pardes.layout;
const Text = pardes.panes.Text;
@@ -65,10 +66,11 @@ pub fn headerText(p: *Pardes, serial: ?u32) ?[]const u8 {
}
pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply {
+ applyTruncation(p, serial);
const text = headerText(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
const out = p.fs.stage(p.gpa);
+ // As a pane tag reads: the text alone, no newline after it.
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);
}
@@ -78,7 +80,19 @@ pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply {
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 };
- if (pardes.ctlfs.pane.tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why);
+ const pending = p.fs.header_trunc_pending == rewriteKey(serial);
+ // The same checks as a pane tag's, whole or not at all; a refused
+ // write drops the truncation it would have come with.
+ if (pardes.ctlfs.pane.tagFault(req.data)) |why| {
+ if (pending) p.fs.header_trunc_pending = null;
+ return tree.failText(req.tag, E.INVAL, why);
+ }
+ const had = if (pending) 0 else headerText(p, serial).?.len;
+ if (req.data.len > limits.max_tag_tail -| had) {
+ if (pending) p.fs.header_trunc_pending = null;
+ return tree.failText(req.tag, E.NOSPC, pardes.ctlfs.pane.e_tag_over);
+ }
+ applyTruncation(p, serial);
const was = headerText(p, serial).?;
var data = req.data;
const rewriting = p.fs.header_rewrite == rewriteKey(serial);
@@ -99,6 +113,20 @@ pub fn write(p: *Pardes, req: Req, serial: ?u32) 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, serial: ?u32) tree.Status {
+ if (header(p, serial) == null) return .err;
+ // Done with the write after it, which may yet be refused.
+ p.fs.header_trunc_pending = rewriteKey(serial);
+ return .ok;
+}
+
+/// A pending truncation of this header, done.
+pub fn applyTruncation(p: *Pardes, serial: ?u32) void {
+ if (p.fs.header_trunc_pending != rewriteKey(serial)) return;
+ p.fs.header_trunc_pending = null;
+ _ = clear(p, serial);
+}
+
+fn clear(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);
@@ -118,6 +146,7 @@ fn rewriteKey(serial: ?u32) u32 {
}
pub fn released(p: *Pardes, serial: ?u32) void {
+ applyTruncation(p, serial);
if (p.fs.header_rewrite != rewriteKey(serial)) return;
p.fs.header_rewrite = null;
p.fs.header_held = false;
@@ -176,7 +205,8 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply {
if (paneWord(line, if (serial == null) pardes.config.topbar_str else tagline.column_tag_default))
return pardes.ctlfs.ctl.refuseTo(p, req, "not a session control message", line, "pane/<n>/ctl");
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);
+ // With no pane at all, a word runs at the stand-in, as a click in
+ // the empty window's tag does (runBuiltinFrom).
p.exec_column = col;
defer p.exec_column = null;
p.exec_header = true;
@@ -198,6 +228,19 @@ test "New written to a column's exec makes its scratch in the session's director
try testing.expectEqualStrings("/project/src/+New", p.panes[p.active].?.file.?.path);
}
+test "a tagexec word that makes, acts on and focuses no pane reads back nothing" {
+ 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.wr(p, tagexec, "Newcol\n").reply.status);
+ try testing.expectEqualStrings("", th.rd(p, tagexec, 0, 64).bytes);
+ // A column's New makes one, and answers it.
+ const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status);
+ var want: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{p.panes[p.active].?.serial}), th.rd(p, col_exec, 0, 64).bytes);
+}
+
test "New written to a column's ctl reads back the new pane alone, as its exec does" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();
@@ -218,6 +261,8 @@ fn paneWord(line: []const u8, own: []const u8) bool {
const word = cmd[0 .. std.mem.indexOfAny(u8, cmd, " \t+") orelse cmd.len];
const b = std.meta.stringToEnum(pardes.builtins.registry.Builtin(), word) orelse return false;
if (pardes.builtins.registry.scope(b) == .session) return false;
+ // A new pane is every tag's to make, in the session's directory.
+ if (b == .New or b == .Tty) return false;
var words = std.mem.tokenizeScalar(u8, own, ' ');
while (words.next()) |w| if (std.mem.eql(u8, w, word)) return false;
return true;
@@ -238,6 +283,47 @@ test "a pane's word written to /tagexec or a column's exec is refused, and acts
try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status);
}
+test "Help and Find at /tagexec act in the session's directory, not the keyboard pane's" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "needle.txt", .data = "n\n" });
+ var buf: [4096]u8 = undefined;
+ const session = buf[0..try tmp.dir.realPath(testing.io, &buf)];
+ @memcpy(p.session_dir[0..session.len], session);
+ p.session_dir_len = @intCast(session.len);
+ const tagexec = @intFromEnum(tree.TopFile.tagexec);
+ for ([_][]const u8{ "Help\n", "Find needle\n" }) |line| {
+ try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, line).reply.status);
+ const made = p.panes[p.active].?.file.?.path;
+ try testing.expectEqualStrings(session, std.fs.path.dirname(made).?);
+ }
+}
+
+test "New and Tty run at /tagexec and a column's exec, with no pane at all too" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const tagexec = @intFromEnum(tree.TopFile.tagexec);
+ const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec);
+ for ([_]u64{ tagexec, col_exec }) |node| for ([_][]const u8{ "New\n", "Tty\n" }) |line| {
+ const before = p.next_serial;
+ try testing.expectEqual(tree.Status.ok, th.wr(p, node, line).reply.status);
+ try testing.expectEqual(before + 1, p.next_serial);
+ };
+ // Every pane gone: the workspace tag makes one all the same.
+ for ([_][]const u8{ "New\n", "Tty\n" }) |line| {
+ while (p.ncol > 0) {
+ const first = p.panes[p.col_panes[0][0]].?.serial;
+ _ = th.wr(p, tree.Node.of(first, .ctl), "delete\n");
+ if (p.anyPane() == null) break;
+ }
+ try testing.expect(p.anyPane() == null);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, line).reply.status);
+ try testing.expect(p.anyPane() != null);
+ }
+}
+
test "every pane word a tag no pane owns does not hold is refused there, Edit included, and the pane is untouched" {
const p = try th.withFile(testing.allocator, "keep me\n");
defer p.deinit();
@@ -250,6 +336,7 @@ test "every pane word a tag no pane owns does not hold is refused there, Edit in
var checked: usize = 0;
for (std.enums.values(Builtin)) |b| {
if (pardes.builtins.registry.scope(b) == .session) continue;
+ if (b == .New or b == .Tty) continue; // every tag's
var owned = false;
var words = std.mem.tokenizeScalar(u8, tag_file.own, ' ');
while (words.next()) |w| owned = owned or std.mem.eql(u8, w, @tagName(b));
@@ -321,15 +408,15 @@ test "a column's tag and the workspace's read, replace and append as a pane tag
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);
+ try testing.expectEqualStrings("New Tty Find Grep Joincol Delcol", 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);
+ try testing.expectEqualStrings("Make", 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);
+ try testing.expectEqualStrings("Make Lint Now", 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");
@@ -356,6 +443,35 @@ test "a focus write makes its pane's column the active one: layout says so and p
}
}
+test "a refused write after a truncation leaves every kind of tag as it was, and Dump still works" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = th.serialOf(p);
+ const col = layout.columnSerial(p, 0);
+ const big = "w" ** (limits.max_tag_tail + 1);
+ // Each tag: a truncating open, then a write too long: nothing changes.
+ for ([_]u64{ Node.of(serial, .tag), Node.ofCol(col, .tag), @intFromEnum(tree.TopFile.tag) }) |node| {
+ _ = th.wr(p, node, " Mine");
+ const before = try testing.allocator.dupe(u8, th.rd(p, node, 0, 1 << 16).bytes);
+ defer testing.allocator.free(before);
+ _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = node, .truncate = true });
+ const h = th.call(p, .{ .tag = 2, .op = .open, .node = node, .omode = 1 }).reply.handle;
+ const r = th.call(p, .{ .tag = 3, .op = .write, .node = node, .handle = h, .data = big });
+ try testing.expectEqual(E.NOSPC, r.errno());
+ try testing.expectEqualStrings("tag: no space: over 4096 bytes", r.reply.ename);
+ _ = th.call(p, .{ .tag = 4, .op = .release, .node = node, .handle = h, .opened = true });
+ try testing.expectEqualStrings(before, th.rd(p, node, 0, 1 << 16).bytes);
+ // A control character is refused the same way on every kind.
+ try testing.expectEqual(E.INVAL, th.wr(p, node, "a\x01b").errno());
+ // A truncation with no write after it clears at the close.
+ _ = th.call(p, .{ .tag = 5, .op = .setattr, .node = node, .truncate = true });
+ const e = th.call(p, .{ .tag = 6, .op = .open, .node = node, .omode = 1 }).reply.handle;
+ _ = th.call(p, .{ .tag = 7, .op = .release, .node = node, .handle = e, .opened = true });
+ try testing.expect(std.mem.indexOf(u8, th.rd(p, node, 0, 1 << 16).bytes, "Mine") == null);
+ }
+ try pardes.dump.dumpState(p);
+}
+
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();