summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig12
-rw-r--r--src/pardes.zig49
-rw-r--r--test/snapshots/badutf.golden62
-rw-r--r--test/snapshots/badutf.snap16
-rw-r--r--test/snapshots/reflow.golden25
-rw-r--r--test/snapshots/reflow.snap20
-rw-r--r--test/snapshots/tinywin.golden97
-rw-r--r--test/snapshots/tinywin.snap31
-rw-r--r--test/snapshots/yankpaste.golden31
-rw-r--r--test/snapshots/yankpaste.snap9
10 files changed, 341 insertions, 11 deletions
diff --git a/build.zig b/build.zig
index c62ee222..a4d530f1 100644
--- a/build.zig
+++ b/build.zig
@@ -314,10 +314,20 @@ pub fn build(b: *std.Build) void {
// consumed, and the defaults otherwise drag ghostty's app graph into the
// build — gtk4 header translation via host pkg-config for linux targets,
// the Xcode app graph (iOS SDK, xcodebuild) on darwin hosts.
+ // NEVER hand ghostty `.Debug`: that flips its `slow_runtime_safety`, which
+ // walks the whole PageList after every mutation and PANICS the app on a
+ // transient state its own next lines repair — `PageList.resizeCols` grows
+ // rows BEFORE it moves a history viewport pin back into the active area,
+ // so widening a window whose scrollback holds wrapped lines dies with
+ // "PageList integrity check failed: ViewportPinInsufficientRows". Those
+ // checks are a ghostty-development tool (upstream ships them off); Zig's
+ // own safety checks come from OUR optimize mode and are unaffected, since
+ // ghostty-vt is a module compiled into this binary. See reflow.snap.
+ const ghostty_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseSafe else optimize;
const ghostty_dep = if (is_emscripten)
b.lazyDependency("ghostty", .{ .simd = false })
else
- b.lazyDependency("ghostty", .{ .target = target, .optimize = optimize, .simd = ghostty_simd, .@"app-runtime" = .none, .@"emit-xcframework" = false });
+ b.lazyDependency("ghostty", .{ .target = target, .optimize = ghostty_optimize, .simd = ghostty_simd, .@"app-runtime" = .none, .@"emit-xcframework" = false });
if (ghostty_dep) |dep| {
const ghostty_vt = dep.module("ghostty-vt");
ghostty_vt_for_snap = ghostty_vt;
diff --git a/src/pardes.zig b/src/pardes.zig
index 8a947065..4d821e60 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -399,13 +399,28 @@ pub const Surface = struct {
/// Print UTF-8 text into a row, no wrap, clipped to [x, x+w). Returns the
/// column after the last written cell. Wide glyphs take two cells.
+ ///
+ /// The text is NOT trusted to be valid UTF-8 — a file pane holds whatever
+ /// bytes are on disk (latin-1 source, an ELF opened by mistake), a path can
+ /// be any bytes at all, and a search row splices both. std's unchecked
+ /// iterator panics on a bad start byte, so decode by hand and paint one
+ /// U+FFFD per undecodable byte (what a terminal does).
fn print(s: *Surface, x: u16, y: u16, w: u16, text: []const u8, style: CellStyle) u16 {
var col = x;
const end = x + w;
- var it = std.unicode.Utf8View.initUnchecked(text).iterator();
- while (it.nextCodepointSlice()) |cp_slice| {
+ var i: usize = 0;
+ while (i < text.len) {
if (col >= end) break;
- const cp = std.unicode.utf8Decode(cp_slice) catch continue;
+ // n == 0: not a start byte at all. A short tail or a bad
+ // continuation decodes to null the same way — one U+FFFD, one byte.
+ const n = std.unicode.utf8ByteSequenceLength(text[i]) catch 0;
+ const decoded: ?u21 = if (n > 0 and i + n <= text.len)
+ (std.unicode.utf8Decode(text[i .. i + n]) catch null)
+ else
+ null;
+ const cp_slice = if (decoded == null) "\u{FFFD}" else text[i .. i + n];
+ i += if (decoded == null) 1 else n;
+ const cp = decoded orelse 0xFFFD;
if (cp == '\r') continue;
const width: u16 = if (cp < 0x80) 1 else uucode.get(.width, cp);
if (width == 0) continue;
@@ -3292,7 +3307,10 @@ pub const Pardes = struct {
const row0 = eb.row0;
if (y[y.len - 1] == '\n') {
const block_text = y[0 .. y.len - 1];
- const n = modal.lineCount(block_text);
+ // a yanked BLANK line is "\n": the block is empty and lineCount
+ // says 0 lines, but it still pastes as one (empty) line — without
+ // the floor every `n - 1` below underflows and panics.
+ const n = @max(1, modal.lineCount(block_text));
var out: []u8 = undefined;
if (before) {
const row: usize = @intCast(@max(0, b.lo_row - row0));
@@ -5461,7 +5479,8 @@ pub const Pardes = struct {
for (0..p.ncol) |c| {
const last = c + 1 == p.ncol;
const fw = @as(f32, @floatFromInt(p.screen_w)) * p.col_weight[c] / wsum;
- const cw: u16 = if (last) (p.screen_w -| x) else @max(1, @as(u16, @intFromFloat(@round(fw))));
+ const wroom = p.screen_w -| x; // same clamp as the rows below
+ const cw: u16 = if (last) wroom else @min(wroom, @max(1, @as(u16, @intFromFloat(@round(fw)))));
p.col_x[c] = x;
p.col_w[c] = cw;
@@ -5478,7 +5497,14 @@ pub const Pardes = struct {
const pane = p.panes[id] orelse continue;
const lastk = k + 1 == p.col_n[c];
const fh = @as(f32, @floatFromInt(avail_h)) * pane.vweight / vsum;
- const ch: u16 = if (lastk) (p.screen_h -| y) else @max(1, @as(u16, @intFromFloat(@round(fh))));
+ // every pane wants at least one row, so a column with more
+ // panes than the window has rows would walk `y` off the bottom
+ // and hand renderPane a rect outside the surface (assert, then
+ // panic — shrink a window with a few stacked panes). Clamp to
+ // what is left: the panes past the edge get h = 0 and render
+ // nothing until the window grows back.
+ const room = p.screen_h -| y;
+ const ch: u16 = if (lastk) room else @min(room, @max(1, @as(u16, @intFromFloat(@round(fh)))));
p.rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch };
y +|= ch;
}
@@ -5558,17 +5584,20 @@ pub const Pardes = struct {
};
// resize-handle hint / drag previews: a dash overlay that keeps the
- // underlying colors (border drags + hover), or the move indicator
+ // underlying colors (border drags + hover), or the move indicator. A
+ // drag holds the coordinates of the last mouse event, so a resize
+ // mid-drag (tiling WM, font-size change) can leave them off the new
+ // surface — every arm below checks before it draws.
switch (p.drag) {
- .border_v => |d| {
+ .border_v => |d| if (d.cur_x < s.cols) {
var row: u16 = TOPBAR_H;
while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎");
},
- .border_h => |d| {
+ .border_h => |d| if (d.cur_y < s.rows) {
var col = p.col_x[d.col];
while (col < p.col_x[d.col] + p.col_w[d.col]) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
},
- .move => |d| {
+ .move => |d| if (d.cur_x < s.cols) {
if (p.movePlacement(d.id, d.cur_x, d.cur_y)) |placement| {
var col: u16 = p.col_x[placement.preview_col];
while (col < p.col_x[placement.preview_col] + p.col_w[placement.preview_col]) : (col += 1) {
diff --git a/test/snapshots/badutf.golden b/test/snapshots/badutf.golden
new file mode 100644
index 00000000..3873f5e8
--- /dev/null
+++ b/test/snapshots/badutf.golden
@@ -0,0 +1,62 @@
+== snap badbytes grid=100x30 cursor=7,2
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/badutf/cwd/bad.txt Save Del NOR /tmp/pardes-snap/badutf/cwd Del
+| 1 hello �� world
+| 2 latin-1 caf� tail bad.txt wide.txt
+| 3 truncated �� here
+| 4 end
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap hscroll-midglyph grid=100x30 cursor=46,2
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/badutf/cwd/wide.txt Save De NOR /tmp/pardes-snap/badutf/cwd Del
+| 1 ��語 日本語 日本語 日本語 tail
+| 2 bad.txt wide.txt
+| 3
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/snapshots/badutf.snap b/test/snapshots/badutf.snap
new file mode 100644
index 00000000..db9447b6
--- /dev/null
+++ b/test/snapshots/badutf.snap
@@ -0,0 +1,16 @@
+# Invalid UTF-8 must RENDER, never panic. Two sources, both ordinary use:
+# a file pane holds whatever bytes are on disk (latin-1, a truncated sequence,
+# an ELF opened by mistake), and the byte-column hscroll cuts a multi-byte
+# glyph in half by design. Undecodable bytes come out as U+FFFD, one per byte.
+file bad.txt hello \xff\xfe world\nlatin-1 caf\xe9 tail\ntruncated \xe6\x97 here\nend\n
+file wide.txt \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e tail\nsecond line\n
+start 30 100 bad.txt
+wait 8000 Kill Newcol
+stable 700 20000
+snap badbytes
+start 30 100 wide.txt
+wait 8000 Kill Newcol
+stable 700 20000
+key $
+stable 700 10000
+snap hscroll-midglyph
diff --git a/test/snapshots/reflow.golden b/test/snapshots/reflow.golden
new file mode 100644
index 00000000..e38657cf
--- /dev/null
+++ b/test/snapshots/reflow.golden
@@ -0,0 +1,25 @@
+== snap widened grid=200x24 cursor=4,19
+|Kill Newcol Tutor Debug NextColor Dump
+| TTY /tmp/pardes-snap/reflow/cwd Del
+| $ printf 'y%.0s' $(seq 1 3000); echo do''ne
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyydone
+| $
+|
+|
+|
+|
diff --git a/test/snapshots/reflow.snap b/test/snapshots/reflow.snap
new file mode 100644
index 00000000..0fa4a30d
--- /dev/null
+++ b/test/snapshots/reflow.snap
@@ -0,0 +1,20 @@
+# Widening a window whose scrollback holds WRAPPED lines, with the viewport
+# scrolled into history: ghostty's resizeCols unwraps, then grows rows while
+# the history viewport pin is still short, and its Debug-only PageList
+# integrity check panicked the whole app ("ViewportPinInsufficientRows").
+# build.zig now pins the ghostty dependency at ReleaseSafe so those
+# development-only checks are off — the app's own safety checks are unchanged.
+start 24 40
+wait 8000 $
+text printf 'y%.0s' $(seq 1 3000); echo do''ne
+key enter
+settle 2500
+stable 700 15000
+wheel up 10 10
+wheel up 10 10
+wheel up 10 10
+wheel up 10 10
+stable 500 8000
+resize 24 200
+stable 700 15000
+snap widened
diff --git a/test/snapshots/tinywin.golden b/test/snapshots/tinywin.golden
new file mode 100644
index 00000000..928241b2
--- /dev/null
+++ b/test/snapshots/tinywin.golden
@@ -0,0 +1,97 @@
+== snap stacked grid=100x30 cursor=4,14
+|Kill Newcol Tutor Debug NextColor Dump
+| TTY /tmp/pardes-snap/tinywin/cwd Del
+| $
+| NOR /tmp/pardes-snap/tinywin/cwd Del
+|
+|
+| NOR /tmp/pardes-snap/tinywin/cwd Del
+|
+|
+| NOR /tmp/pardes-snap/tinywin/cwd Del
+|
+|
+| NOR /tmp/pardes-snap/tinywin/cwd Del
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap shrunk grid=60x3 cursor=59,2
+|Kill Newcol Tutor Debug NextColor Dump
+| TTY /tmp/pardes-snap/tinywin/cwd Del
+| NOR /tmp/pardes-snap/tinywin/cwd Del
+== snap columns grid=100x30 cursor=79,3
+|Kill Newcol Tutor Debug NextColor Dump
+| TTY /tmp/pardes-snap/ti NOR /tmp/pardes-snap/ti NOR /tmp/pardes-snap/ti NOR /tmp/pardes-snap/ti
+| $
+| NOR /tmp/pardes-snap/ti
+|
+|
+| NOR /tmp/pardes-snap/ti
+|
+|
+| NOR /tmp/pardes-snap/ti
+|
+|
+| NOR /tmp/pardes-snap/ti
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap narrow grid=4x30 cursor=3,29
+|Kill
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
+|╎
diff --git a/test/snapshots/tinywin.snap b/test/snapshots/tinywin.snap
new file mode 100644
index 00000000..fab7f950
--- /dev/null
+++ b/test/snapshots/tinywin.snap
@@ -0,0 +1,31 @@
+# Shrinking the window past "one row per pane" used to panic: computeGeom gave
+# every pane in a column at least one row, so `y` walked off the bottom and
+# renderPane got a rect outside the surface (assert). Panes that no longer fit
+# now get h = 0 and simply do not render until the window grows back.
+start 30 100
+wait 8000 $
+key a-n
+settle 400
+key a-n
+settle 400
+key a-n
+settle 400
+key a-n
+stable 700 15000
+snap stacked
+resize 3 60
+stable 700 10000
+snap shrunk
+# ...and the same sideways: more columns than the window is wide
+resize 30 100
+stable 700 10000
+key space c n
+settle 600
+key space c n
+settle 600
+key space c n
+stable 700 15000
+snap columns
+resize 30 4
+stable 700 10000
+snap narrow
diff --git a/test/snapshots/yankpaste.golden b/test/snapshots/yankpaste.golden
index 213495dc..3f335d56 100644
--- a/test/snapshots/yankpaste.golden
+++ b/test/snapshots/yankpaste.golden
@@ -91,3 +91,34 @@
|
|
|
+== snap blankline grid=100x30 cursor=7,4
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/yankpaste/cwd/f.txt Save De NOR /tmp/pardes-snap/yankpaste/cwd Del
+| 1 liline 1
+| 2 f.txt
+| 3
+| 4 line 2
+| 5 line 3
+| 6 line 2
+| 7 line 4
+| 8 line 2
+| 9 line 4
+| 10 line 5
+| 11
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/snapshots/yankpaste.snap b/test/snapshots/yankpaste.snap
index 38ca6e3b..96082ad6 100644
--- a/test/snapshots/yankpaste.snap
+++ b/test/snapshots/yankpaste.snap
@@ -26,3 +26,12 @@ stable 400 5000
key v l y p
stable 400 5000
snap charwise-inline
+# a BLANK line yanks as a bare "\n": the block is empty, lineCount says 0
+# lines, and every `n - 1` in the linewise paste used to underflow — panic on
+# y then p. It pastes as one empty line.
+key o
+key esc
+settle 700
+key y p
+stable 400 5000
+snap blankline