summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md6
-rw-r--r--src/Terminal.zig60
-rw-r--r--src/fs-help.txt2
-rw-r--r--src/fs.zig2
-rw-r--r--src/ninep/screen.zig22
5 files changed, 75 insertions, 17 deletions
diff --git a/docs/fs.md b/docs/fs.md
index a5d3c4ca..a4fec333 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -375,7 +375,11 @@ selection, and leaves `look` reading back empty.
`/pane/<n>/name` reads the pane's file name (a terminal's directory) and
writing it renames the buffer; a relative name resolves against the pane's
-directory. `body` appends on write and replaces on truncating open. `sel`
+directory. `body` appends on write and replaces on truncating open. A
+terminal's `body` is its history as plain text, frozen per open, in logical
+lines: a row the terminal wrapped is joined back to the row before it (the
+wrap is ghostty's, as `pty/run`'s output unwraps), and the last line ends
+with a newline. `sel`
reads the selected text and writing it replaces the selection. `errors`
appends to the directory's `+Errors` pane. Holding `event` open redirects the
pane's Look and Exec clicks to that client, and so does a line written to the
diff --git a/src/Terminal.zig b/src/Terminal.zig
index 504ab887..4808298e 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -385,26 +385,66 @@ pub fn mouseFormatSgr(pane: *const Pane) bool {
return state.vt.modes.get(.mouse_format_sgr);
}
-/// The whole scrollback as plain text, `gpa`-owned: what `Save` writes out.
+/// The whole scrollback as plain text, `gpa`-owned: what a terminal's body
+/// reads and `Save` writes out. Logical lines: rows the terminal wrapped
+/// are joined again (ghostty's wrap flags, as pty/run's output is), and
+/// the last line ends with a newline.
pub fn screenTextAlloc(pane: *Pane, gpa: std.mem.Allocator) ![]const u8 {
if (comptime !enabled) return &.{};
- const state = pane.terminal orelse return &.{};
- return state.vt.screens.active.dumpStringAlloc(gpa, .{ .screen = .{} });
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ errdefer out.deinit();
+ try writeScreenText(pane, &out.writer);
+ return out.toOwnedSlice();
}
+fn writeScreenText(pane: *Pane, w: *std.Io.Writer) !void {
+ if (comptime !enabled) return;
+ const state = pane.terminal orelse return;
+ const screen = state.vt.screens.active;
+ const br = screen.pages.getBottomRight(.screen) orelse return;
+ var both: TeeWriter = .{ .a = w };
+ try screen.dumpString(&both.writer, .{ .tl = screen.pages.getTopLeft(.screen), .br = br, .unwrap = true });
+ if (both.count > 0 and both.final != '\n') try w.writeByte('\n');
+}
+
+/// Passes bytes through, remembering how many and the last one.
+const TeeWriter = struct {
+ a: *std.Io.Writer,
+ count: usize = 0,
+ final: u8 = 0,
+ writer: std.Io.Writer = .{ .vtable = &.{ .drain = drain }, .buffer = &.{} },
+
+ fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
+ const t: *TeeWriter = @alignCast(@fieldParentPtr("writer", w));
+ var n: usize = 0;
+ for (data[0 .. data.len - 1]) |bytes| {
+ try t.a.writeAll(bytes);
+ t.note(bytes);
+ n += bytes.len;
+ }
+ const pattern = data[data.len - 1];
+ for (0..splat) |_| {
+ try t.a.writeAll(pattern);
+ t.note(pattern);
+ n += pattern.len;
+ }
+ return n;
+ }
+
+ fn note(t: *TeeWriter, bytes: []const u8) void {
+ if (bytes.len == 0) return;
+ t.count += bytes.len;
+ t.final = bytes[bytes.len - 1];
+ }
+};
+
/// How long `screenTextAlloc` would be, counted without keeping it: the
/// length a terminal's body reads as, which its ctl line reports.
pub fn screenTextLen(pane: *Pane) usize {
if (comptime !enabled) return 0;
- const state = pane.terminal orelse return 0;
- const screen = state.vt.screens.active;
var buf: [256]u8 = undefined;
var counting: std.Io.Writer.Discarding = .init(&buf);
- screen.dumpString(&counting.writer, .{
- .tl = screen.pages.getTopLeft(.screen),
- .br = screen.pages.getBottomRight(.screen) orelse return 0,
- .unwrap = false,
- }) catch return 0;
+ writeScreenText(pane, &counting.writer) catch return 0;
return @intCast(counting.fullCount());
}
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 50dc5d9c..50e9e7fc 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -41,7 +41,7 @@ Pitfalls, one each:
addr: #n,#m, 12:5 or /re/ from the current address (^ $ per line); > never resets it, 0 does; a miss refuses data.
data: > replaces the addr range, : > deletes it, a 2nd > inserts (addr moved); body: > empties all.
tag reads the path, then its own text; > replaces that text (default words too), >> appends.
- A terminal's body is a history snapshot frozen per open; pty/data is the live stream.
+ A terminal's body is a history snapshot frozen per open, wrapped rows joined into lines; pty/data is the live stream.
exec: a non-builtin line runs as a command pane (ctl's Shell -c; exit N shown, logged), a shell's typed in.
Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why.
lock/unlock need a held ctl fd (exec 3>ctl), a held lock fails: retry. A column's last pane leaves it empty; the last pane of all quits.
diff --git a/src/fs.zig b/src/fs.zig
index 88ffb4a3..65e5d276 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -491,7 +491,7 @@ test "bounded reads release terminal snapshots after success and size refusal" {
defer gpa.free(empty);
try std.testing.expectEqual(@as(usize, 0), empty.len);
p.update(.{ .output = .{ .pane = 0, .bytes = "limited terminal output" } });
- const expected = "limited terminal output";
+ const expected = "limited terminal output\n"; // a line ends with its newline
const bytes = try readLimit(p, path, expected.len);
defer gpa.free(bytes);
try std.testing.expectEqualStrings(expected, bytes);
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index 462ea40f..f496da03 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -146,7 +146,7 @@ test "terminal body handles keep one history snapshot across fragmented reads" {
try testing.expectEqualStrings("", call(p, .{ .tag = 4, .op = .read, .node = node, .handle = opened.reply.handle, .off = original.len, .size = 3 }).bytes);
_ = call(p, .{ .tag = 5, .op = .release, .node = node, .handle = opened.reply.handle });
const newer = call(p, .{ .tag = 6, .op = .open, .node = node });
- try testing.expectEqualStrings("new output", call(p, .{ .tag = 7, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
+ try testing.expectEqualStrings("new output\n", call(p, .{ .tag = 7, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
_ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = newer.reply.handle });
}
@@ -162,7 +162,7 @@ test "an empty terminal body snapshot stays empty while output continues" {
try testing.expectEqualStrings("", call(p, .{ .tag = 3, .op = .read, .node = node, .handle = opened.reply.handle, .size = 32 }).bytes);
_ = call(p, .{ .tag = 4, .op = .release, .node = node, .handle = opened.reply.handle });
const newer = call(p, .{ .tag = 5, .op = .open, .node = node });
- try testing.expectEqualStrings("new output", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
+ try testing.expectEqualStrings("new output\n", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
_ = call(p, .{ .tag = 7, .op = .release, .node = node, .handle = newer.reply.handle });
}
@@ -194,13 +194,13 @@ test "terminal body snapshots are lazy bounded and released after the pane close
try testing.expectEqual(E.NOMEM, failed.errno());
try testing.expect(p.fs.opens[handles[0] - 1].what.snapshot == null);
const first = call(p, .{ .tag = 4, .op = .read, .node = node, .handle = handles[0], .size = 32 });
- try testing.expectEqualStrings("latest output", first.bytes);
+ try testing.expectEqualStrings("latest output\n", first.bytes);
const saved = p.fs.opens[handles[0] - 1].what.snapshot.?;
failing = std.testing.FailingAllocator.init(gpa, .{ .fail_index = 0 });
p.gpa = failing.allocator();
const second = call(p, .{ .tag = 5, .op = .read, .node = node, .handle = handles[0], .off = 7, .size = 32 });
p.gpa = gpa;
- try testing.expectEqualStrings("output", second.bytes);
+ try testing.expectEqualStrings("output\n", second.bytes);
try testing.expect(!failing.has_induced_failure);
try testing.expectEqual(saved.ptr, p.fs.opens[handles[0] - 1].what.snapshot.?.ptr);
try testing.expectEqual(E.INVAL, call(p, .{ .tag = 6, .op = .read, .node = screen_node, .handle = handles[0], .size = 32 }).errno());
@@ -328,3 +328,17 @@ test "screen inspection preserves acknowledged presentation and the next real fr
}
}
}
+
+test "a terminal's body is its logical lines: a row the terminal wrapped is joined again" {
+ const p = try th.withTerm(testing.allocator);
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ const long = "w" ** 50 ++ "x" ** 150; // wider than the pane: it wraps
+ try testing.expect(long.len > pane.cols);
+ p.update(.{ .output = .{ .pane = 0, .bytes = long ++ "\r\nnext" } });
+ while (p.nextEffect()) |_| {}
+ const node = Node.of(pane.serial, .body);
+ const body = th.rd(p, node, 0, 4096).bytes;
+ try testing.expectEqualStrings(long ++ "\nnext\n", body);
+ try testing.expectEqual(@as(u64, body.len), th.call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size);
+}