summaryrefslogtreecommitdiff
path: root/src/ninep/pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/pane.zig')
-rw-r--r--src/ninep/pane.zig61
1 files changed, 53 insertions, 8 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 13381066..c594c7a3 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,
@@ -260,6 +265,13 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 {
// ---- the index ----
+/// The serial of the column a pane is in, as /layout and /col name it; the
+/// last word of its /index line.
+pub fn columnOf(p: *Pardes, id: usize) u32 {
+ const at = pardes.layout.findPane(p, id) orelse return 0;
+ return pardes.layout.columnSerial(p, at.col);
+}
+
pub fn indexLen(p: *Pardes) u64 {
var n: u64 = 0;
var last: u32 = 0;
@@ -269,6 +281,7 @@ pub fn indexLen(p: *Pardes) u64 {
var digits: [16]u8 = undefined;
n += (std.fmt.bufPrint(&digits, "{d}", .{serial}) catch unreachable).len;
n += 1 + kindOf(pane).len + 3 + nameOf(pane).len + 1;
+ n += 1 + (std.fmt.bufPrint(&digits, "{d}", .{columnOf(p, p.paneBySerial(serial).?)}) catch unreachable).len;
}
return n;
}
@@ -427,11 +440,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) };
}
@@ -544,7 +569,10 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const name = std.mem.trim(u8, req.data, " \t\r\n");
if (name.len == 0) return Reply.fail(req.tag, E.INVAL);
for (name) |c| if (c < ' ') return Reply.fail(req.tag, E.INVAL);
- if (fileOf(pane) == null) return Reply.fail(req.tag, E.PERM);
+ if (fileOf(pane) == null) return tree.failText(req.tag, E.PERM, if (pane.isTerminal())
+ "rename not allowed: a terminal is named by its shell's directory; cd there, or Tty in another"
+ else
+ "rename not allowed: an image or PDF is named by the file it shows");
const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), name }) catch
return Reply.fail(req.tag, E.NOMEM);
if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return Reply.fail(req.tag, E.INVAL);
@@ -648,6 +676,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 `>>`.
@@ -681,7 +711,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
const a = rd(p, index, 0, 4096);
try testing.expectEqual(Status.ok, a.reply.status);
var want: [64]u8 = undefined;
- const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt\n", .{pane.serial});
+ const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt {d}\n", .{ pane.serial, columnOf(p, 0) });
try testing.expect(std.mem.indexOf(u8, a.bytes, clean_row) != null);
const at = std.mem.indexOf(u8, a.bytes, clean_row).?;
const stat = call(p, .{ .tag = 1, .op = .getattr, .node = index });
@@ -690,7 +720,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
pane.file.?.saved_revision = pane.file.?.revision -% 1;
var dirty_want: [64]u8 = undefined;
- const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt\n", .{pane.serial});
+ const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt {d}\n", .{ pane.serial, columnOf(p, 0) });
try testing.expect(std.mem.indexOf(u8, rd(p, index, 0, 4096).bytes, dirty_row) != null);
const kind = at + std.mem.indexOfScalar(u8, clean_row, ' ').? + 1;
try testing.expectEqualStrings("text", rd(p, index, kind, 4).bytes);
@@ -700,7 +730,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
try testing.expectEqual(@as(u64, both.bytes.len), indexLen(p));
var expected: [4200]u8 = undefined;
const made = p.panes[p.paneBySerial(serial).?].?;
- try testing.expect(std.mem.endsWith(u8, both.bytes, try std.fmt.bufPrint(&expected, "\n{d} text 0 {s}\n", .{ serial, nameOf(made) })));
+ try testing.expect(std.mem.endsWith(u8, both.bytes, try std.fmt.bufPrint(&expected, "\n{d} text 0 {s} {d}\n", .{ serial, nameOf(made), columnOf(p, p.paneBySerial(serial).?) })));
}
test "a terminal is listed as term with its directory as name" {
@@ -712,10 +742,12 @@ test "a terminal is listed as term with its directory as name" {
try testing.expect(std.mem.indexOf(
u8,
rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes,
- try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}),
+ try std.fmt.bufPrint(&want, "{d} term 0 /work/dir {d}\n", .{ pane.serial, columnOf(p, 0) }),
) != null);
try testing.expectEqualStrings("/work/dir\n", rd(p, Node.of(pane.serial, .name), 0, 4096).bytes);
- try testing.expectEqual(E.PERM, wr(p, Node.of(pane.serial, .name), "/elsewhere\n").errno());
+ const refused = wr(p, Node.of(pane.serial, .name), "/elsewhere\n");
+ try testing.expectEqual(E.PERM, refused.errno());
+ try testing.expect(std.mem.startsWith(u8, refused.reply.ename, "rename not allowed: a terminal is named"));
}
test "body reads at any offset and writes append" {
@@ -977,6 +1009,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" {