summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 19:30:33 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitb2886edda1a1630f38732975c04bdf4d6f6ded29 (patch)
treebf9d63ebb5a1f08fe6b11f5b96a8388945e71b66 /src
parent42b68e5ea63c32ff9359f2268ef0d8c08eaafff2 (diff)
downloadpardes-b2886edda1a1630f38732975c04bdf4d6f6ded29.tar.gz
pardes-b2886edda1a1630f38732975c04bdf4d6f6ded29.zip
A refused write to dot after a shell's truncation changes nothing: the truncation waits for the write, as limit's does
echo /nomatch/ > dot truncated dot to an empty range at the top before the write was refused, so a failed address still moved the selection and scrolled. The truncation is now held until the write that follows it: that write sets dot, or, refused, leaves it as it was. A truncation with no write after it (: > dot) empties it when its open goes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ninep/addr.zig22
-rw-r--r--src/ninep/pane.zig14
-rw-r--r--src/ninep/tree.zig6
3 files changed, 40 insertions, 2 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig
index df2cfaf0..b78c46e7 100644
--- a/src/ninep/addr.zig
+++ b/src/ninep/addr.zig
@@ -498,6 +498,28 @@ test "regular expressions search lines as sam's do, and a search wraps" {
try testing.expectEqual(@as(u32, 5), q.panes[0].?.fs.addr.q0);
}
+test "a refused write to dot after a truncation changes nothing; a truncation with no write empties it at close" {
+ const p = try th.withFile(testing.allocator, "one\ntwo\nthree\n");
+ defer p.deinit();
+ const serial = th.serialOf(p);
+ const dot = Node.of(serial, .dot);
+ try testing.expectEqual(Status.ok, th.wr(p, dot, "2").reply.status);
+ const was = th.rd(p, dot, 0, 64).bytes;
+ var kept: [64]u8 = undefined;
+ @memcpy(kept[0..was.len], was);
+ // A shell's `echo /nomatch/ > dot`: truncate, then the refused write.
+ const h = th.call(p, .{ .tag = 1, .op = .open, .node = dot, .omode = 1 }).reply.handle;
+ _ = th.call(p, .{ .tag = 2, .op = .setattr, .node = dot, .truncate = true });
+ try testing.expectEqual(Status.err, th.call(p, .{ .tag = 3, .op = .write, .node = dot, .handle = h, .data = "/nomatch/" }).reply.status);
+ _ = th.call(p, .{ .tag = 4, .op = .release, .node = dot, .handle = h, .opened = true });
+ try testing.expectEqualStrings(kept[0..was.len], th.rd(p, dot, 0, 64).bytes);
+ // `: > dot`: the truncation alone empties it once its open goes.
+ const h2 = th.call(p, .{ .tag = 5, .op = .open, .node = dot, .omode = 1 }).reply.handle;
+ _ = th.call(p, .{ .tag = 6, .op = .setattr, .node = dot, .truncate = true });
+ _ = th.call(p, .{ .tag = 7, .op = .release, .node = dot, .handle = h2, .opened = true });
+ try testing.expectEqualStrings(" 0 0 ", th.rd(p, dot, 0, 64).bytes);
+}
+
test "a failed address leaves none, so data refuses rather than act at the last one" {
const p = try th.withFile(testing.allocator, "one\ntwo\n");
defer p.deinit();
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 78bec09f..71484538 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -44,6 +44,10 @@ pub const State = struct {
/// The limit a truncation cleared, kept until the write that follows it
/// on that open (a shell's `>`): a write that fails puts it back.
limit_truncated: ?Range = null,
+ /// `dot` was truncated and nothing written since: the write that
+ /// follows sets it or, refused, leaves it as it was; with none, the
+ /// open's release empties it (tree.zig releaseHandle).
+ dot_truncated: bool = false,
/// Opens of `event`, which hold the pane scripted.
readers: u16 = 0,
/// One of `readers` reads `event`; a second reading open is refused.
@@ -800,9 +804,11 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply {
pf.limit = kept;
pf.limit_truncated = null;
};
+ if (file == .dot) pf.dot_truncated = false;
return tree.failText(req.tag, E.INVAL, a.err);
};
if (file == .limit) pf.limit_truncated = null;
+ if (file == .dot) pf.dot_truncated = false;
switch (file) {
.addr => {
pf.addr = range;
@@ -1000,7 +1006,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
pf.limit_truncated = pf.limit;
pf.limit = null;
},
- .dot => if (fileOf(pane) != null) setDot(pane, .{}),
+ .dot => pf.dot_truncated = fileOf(pane) != null,
else => {},
}
return .ok;
@@ -1211,8 +1217,12 @@ test "sel reads the selection and writing sel replaces it" {
try testing.expectEqual(@as(u32, 0), p.panes[0].?.fs.addr.q0);
try testing.expectEqual(@as(u32, 3), p.panes[0].?.fs.addr.q1);
- // A truncation empties dot; an address expression addresses it.
+ // A truncation empties dot once its open goes, nothing written after
+ // it; an address expression addresses it.
+ const h = call(p, .{ .tag = 8, .op = .open, .node = dot, .omode = 1 }).reply.handle;
_ = call(p, .{ .tag = 9, .op = .setattr, .node = dot, .truncate = true });
+ try testing.expectEqualStrings("one", rd(p, sel, 0, 100).bytes);
+ _ = call(p, .{ .tag = 10, .op = .release, .node = dot, .handle = h, .opened = true });
try testing.expectEqualStrings("", rd(p, sel, 0, 100).bytes);
try testing.expectEqual(Status.ok, wr(p, dot, "#0,#3").reply.status);
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 4300d2b5..c0ace5be 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -949,6 +949,12 @@ fn releaseHandle(p: *Pardes, req: Req) void {
if (t == .pane and t.pane.file == .limit) if (p.paneBySerial(t.pane.serial)) |id| {
p.panes[id].?.fs.limit_truncated = null;
};
+ // `: > dot` empties it once its open goes, no write having set it.
+ if (t == .pane and t.pane.file == .dot) if (p.paneBySerial(t.pane.serial)) |id| {
+ const pn = p.panes[id].?;
+ if (pn.fs.dot_truncated) pane.setDot(pn, .{});
+ pn.fs.dot_truncated = false;
+ };
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;