summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 23:30:36 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitaae6fc630d9d77de9ad975414d003a55d5b9d1e3 (patch)
treef75bad53a8538f0bff15bf9a6e58b23738f60df2
parentd321916267fc4f0f2d82fbe1fd0d37292d24456b (diff)
downloadpardes-aae6fc630d9d77de9ad975414d003a55d5b9d1e3.tar.gz
pardes-aae6fc630d9d77de9ad975414d003a55d5b9d1e3.zip
A truncating write to tag drops the one newline that ends it
`echo X > tag` stored X and a trailing newline, which drew an empty tag row. After a truncation the newline ending each write is held back until more text follows or the open is released, so the one ending the whole of it is dropped and any other stays; `>>` keeps what it is given. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md4
-rw-r--r--src/ninep/pane.zig36
-rw-r--r--src/ninep/tree.zig8
3 files changed, 44 insertions, 4 deletions
diff --git a/docs/fs.md b/docs/fs.md
index c257a55d..5afb2614 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -505,7 +505,9 @@ then the text you may edit. A write appends to that text,
newlines included, and a tag with more than one line takes a row per line on
screen; truncating `tag` clears it, as acme's `cleartag` does -- the default
words (`Del`, `Put` and the rest) with it, since they are that text until
-you edit it, so `echo Make > tag` leaves only `Make`; append with `>>` to
+you edit it, so `echo Make > tag` leaves only `Make` -- a truncating write
+drops the one newline that ends what it wrote, which would draw an empty
+row, and keeps any other; append with `>>` to
keep them, with `printf ' Make' >> tag`: `echo` ends its word with a
newline, which starts a new line of the tag. The clearing is an edit of the tag like a typed one and its undo
history is kept: `u` in the tag brings back the text it cleared, words
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 13381066..9d20c587 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -63,6 +63,11 @@ pub const State = struct {
lock: ?u32 = null,
/// The host started a shell it could not teach to mark its prompts.
unmarked: bool = false,
+ /// `tag` was truncated (`echo X > tag`): until that open is released,
+ /// the newline that ends a write is held back, so the one that ends the
+ /// whole of it is dropped and draws no empty row.
+ tag_rewrite: bool = false,
+ tag_held_newline: bool = false,
/// The pty's size as pty/ctl's winsize last set it, until the pane's own
/// grid resizes and gives the pty its size again.
winsize: ?[2]u16 = null,
@@ -427,11 +432,23 @@ fn writeTag(req: Req, pane: *Pane) Reply {
if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 };
// The tag's own text grows by what is written, newlines and all: a tag
// is a text like any other (acme's tag file appends the same way).
+ const pf = &pane.fs;
const had = tagline.curTail(pane).len;
- const room = limits.max_tag_tail -| had;
+ const room = limits.max_tag_tail -| had -| @intFromBool(pf.tag_held_newline);
if (room == 0) return Reply.fail(req.tag, E.NOSPC);
const take = wholeUtf8(req.data[0..@min(req.data.len, room)]);
- pane.appendTag(req.data[0..take]) catch return Reply.fail(req.tag, E.NOMEM);
+ // A truncating write drops ONE trailing newline, its whole text's: a
+ // newline held from a write before goes in once more text follows it.
+ if (pf.tag_held_newline) {
+ pane.appendTag("\n") catch return Reply.fail(req.tag, E.NOMEM);
+ pf.tag_held_newline = false;
+ }
+ var text = req.data[0..take];
+ if (pf.tag_rewrite and text.len > 0 and text[text.len - 1] == '\n') {
+ text = text[0 .. text.len - 1];
+ pf.tag_held_newline = true;
+ }
+ pane.appendTag(text) catch return Reply.fail(req.tag, E.NOMEM);
return .{ .tag = req.tag, .written = @intCast(take) };
}
@@ -648,6 +665,8 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
pane.tag.cur_col = if (tagline.pathPrefix(p.scratch.allocator(), pane)) |prefix| @intCast(prefix.len) else |_| 0;
pane.tag.vsel.active = false;
pane.tag.nsel = 0;
+ pf.tag_rewrite = true;
+ pf.tag_held_newline = false;
},
// A shell's `>` truncates before it writes: the address written
// is evaluated from where the last one left off, as with `>>`.
@@ -977,6 +996,19 @@ test "truncating the tag clears its editable tail" {
// and a click on the G puts the tag's cursor on it
const at = tagline.tagOffsetAt(p, pane, 0, "/test.txt ".len).?;
try testing.expectEqualStrings("Get", whole[at.col..]);
+
+ // `echo Make > tag`: the newline ending the truncating write's text is
+ // dropped, one only; a newline between its writes stays.
+ _ = call(p, .{ .tag = 3, .op = .release, .node = tag });
+ _ = call(p, .{ .tag = 4, .op = .setattr, .node = tag, .truncate = true });
+ _ = wr(p, tag, "Make\n");
+ try testing.expectEqualStrings("Make", pane.tag.own.?);
+ _ = wr(p, tag, "More\n\n");
+ try testing.expectEqualStrings("Make\nMore\n", pane.tag.own.?);
+ _ = call(p, .{ .tag = 5, .op = .release, .node = tag });
+ // `>>` keeps what it is given.
+ _ = wr(p, tag, " Tail\n");
+ try testing.expectEqualStrings("Make\nMore\n Tail\n", pane.tag.own.?);
}
test "name reads the file name and writing it promotes a scratch without touching its body" {
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index ad95f6bf..e5320f61 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -676,8 +676,14 @@ fn release(p: *Pardes, req: Req) Reply {
/// its releases before the connection's slot is reused, so none outlives
/// the connection it came on.
fn releaseHandle(p: *Pardes, req: Req) void {
- const o = openOf(p, req) orelse return;
const t = Node.target(req.node).?;
+ // A truncating write to `tag` is over when its open goes (pane.zig
+ // writeTag): a newline it held back is dropped.
+ if (t == .pane and t.pane.file == .tag) if (p.paneBySerial(t.pane.serial)) |id| {
+ p.panes[id].?.fs.tag_rewrite = false;
+ p.panes[id].?.fs.tag_held_newline = false;
+ };
+ const o = openOf(p, req) orelse return;
const of: ?*Pane = if (t == .pane) (if (p.paneBySerial(t.pane.serial)) |id| p.panes[id] else null) else null;
if (of) |pn| switch (o.what) {
.event => |reader| {