diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-04 16:32:43 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-04 16:32:43 +0300 |
| commit | 648afa1fd5b4c2eb74b4292b4eceeeb6d93016c1 (patch) | |
| tree | 368bce0f173319b98bce0e99f0c829fcd54520b3 /vendor | |
| parent | f5f753c7286ff4cc30cfe3a9d48c053bfe2881c7 (diff) | |
fix: backport libghostty resize overflow fix
refs #465
Diffstat (limited to 'vendor')
| -rw-r--r-- | vendor/libghostty-vt.patches.md | 40 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/PageList.zig | 35 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/terminal.zig | 23 | ||||
| -rw-r--r-- | vendor/patches/libghostty-vt/0001-backport-resizecols-cursor-subtraction.patch | 115 |
4 files changed, 211 insertions, 2 deletions
diff --git a/vendor/libghostty-vt.patches.md b/vendor/libghostty-vt.patches.md new file mode 100644 index 00000000..98b7569e --- /dev/null +++ b/vendor/libghostty-vt.patches.md @@ -0,0 +1,40 @@ +# libghostty-vt local patches + +This file tracks intentional local changes applied on top of the vendored +`libghostty-vt` source. Remove a patch only when the vendored source commit +contains the upstream fix and the listed verification still passes. + +## 0001 backport resizeCols cursor subtraction saturation + +status: active + +patch: `vendor/patches/libghostty-vt/0001-backport-resizecols-cursor-subtraction.patch` + +herdr issue: https://github.com/ogulcancelik/herdr/issues/465 + +upstream discussion: https://github.com/ghostty-org/ghostty/discussions/12905 + +upstream pr: https://github.com/ghostty-org/ghostty/pull/12907 + +introduced upstream: `c44afa625` + +vendored base: `0f7cd84b880b203c98683e520e84b9db0c5938d8` + +local files: + +- `vendor/libghostty-vt/src/terminal/PageList.zig` +- `vendor/libghostty-vt/src/terminal/c/terminal.zig` + +reason: shrinking rows and columns in one resize can leave the pre-resize +cursor row past the new row count. `PageList.resizeCols` then computed rows +below the cursor with checked unsigned subtraction and aborted in safety builds. + +remove when: the vendored source commit contains upstream PR #12907 and the +local ReleaseSafe resize regression tests pass without this patch. + +verification: + +```sh +zig build test-lib-vt -Demit-lib-vt -Doptimize=ReleaseSafe -Dtest-filter="resize shrinks both axes with cursor at bottom" +zig build test-lib-vt -Demit-lib-vt -Doptimize=ReleaseSafe -Dtest-filter="PageList resize less rows and cols cursor at bottom" +``` diff --git a/vendor/libghostty-vt/src/terminal/PageList.zig b/vendor/libghostty-vt/src/terminal/PageList.zig index 8e5cd193..63155962 100644 --- a/vendor/libghostty-vt/src/terminal/PageList.zig +++ b/vendor/libghostty-vt/src/terminal/PageList.zig @@ -1059,7 +1059,7 @@ fn resizeCols( break :cursor .{ .tracked_pin = c.pin orelse try self.trackPin(p), .untrack = c.pin == null, - .remaining_rows = self.rows - c.y - 1, + .remaining_rows = self.rows -| (c.y + 1), .wrapped_rows = wrapped, }; } else null; @@ -1192,7 +1192,7 @@ fn resizeCols( break :wrapped wrapped; }; - const current = self.rows - active_pt.active.y - 1; + const current = self.rows -| (active_pt.active.y + 1); var req_rows = c.remaining_rows; req_rows -|= wrapped -| c.wrapped_rows; @@ -10828,6 +10828,37 @@ test "PageList resize (no reflow) less rows and cols" { } } +test "PageList resize less rows and cols cursor at bottom" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + const cursor_pin = try s.trackPin(s.pin(.{ .active = .{ + .x = 0, + .y = s.rows - 1, + } }).?); + defer s.untrackPin(cursor_pin); + + // Shrink both axes so the original cursor.y is strictly past the new row + // count, after resizeWithoutReflow has already reduced self.rows. + try s.resize(.{ + .cols = 79, + .rows = 20, + .reflow = true, + .cursor = .{ .x = 0, .y = 23, .pin = cursor_pin }, + }); + try testing.expectEqual(@as(usize, 79), s.cols); + try testing.expectEqual(@as(usize, 20), s.rows); + + // remaining_rows saturates to 0, so the cursor lands on the new bottom row. + try testing.expectEqual(point.Point{ .active = .{ + .x = 0, + .y = s.rows - 1, + } }, s.pointFromPin(.active, cursor_pin.*).?); +} + test "PageList resize (no reflow) more rows and less cols" { const testing = std.testing; const alloc = testing.allocator; diff --git a/vendor/libghostty-vt/src/terminal/c/terminal.zig b/vendor/libghostty-vt/src/terminal/c/terminal.zig index 5a5db2d6..39959f83 100644 --- a/vendor/libghostty-vt/src/terminal/c/terminal.zig +++ b/vendor/libghostty-vt/src/terminal/c/terminal.zig @@ -1005,6 +1005,29 @@ test "resize invalid value" { try testing.expectEqual(Result.invalid_value, resize(t, 80, 0, 9, 18)); } +test "resize shrinks both axes with cursor at bottom" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }, + )); + defer free(t); + + // CSI 24;1H parks the cursor on the bottom row. + const move = "\x1b[24;1H"; + vt_write(t, move, move.len); + + // Shrink both axes; pre-resize cursor.y sits past the new bottom row. + try testing.expectEqual(Result.success, resize(t, 79, 23, 8, 16)); + try testing.expectEqual(79, t.?.terminal.cols); + try testing.expectEqual(23, t.?.terminal.rows); +} + test "mode_get and mode_set" { var t: Terminal = null; try testing.expectEqual(Result.success, new( diff --git a/vendor/patches/libghostty-vt/0001-backport-resizecols-cursor-subtraction.patch b/vendor/patches/libghostty-vt/0001-backport-resizecols-cursor-subtraction.patch new file mode 100644 index 00000000..1f8d0f36 --- /dev/null +++ b/vendor/patches/libghostty-vt/0001-backport-resizecols-cursor-subtraction.patch @@ -0,0 +1,115 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: herdr maintainers <maintainers@herdr.dev> +Date: Thu, 4 Jun 2026 00:00:00 +0000 +Subject: [PATCH] backport resizeCols cursor subtraction saturation + +Backport ghostty-org/ghostty#12907 onto the vendored libghostty-vt source. +Shrink-both resize can leave the old cursor row past the new row count after +resizeWithoutReflow reduces self.rows. Saturating subtraction treats rows below +that cursor as zero instead of trapping in safety builds. + +Herdr issue: https://github.com/ogulcancelik/herdr/issues/465 +Upstream discussion: https://github.com/ghostty-org/ghostty/discussions/12905 +Upstream PR: https://github.com/ghostty-org/ghostty/pull/12907 +Vendored base: 0f7cd84b880b203c98683e520e84b9db0c5938d8 +--- + vendor/libghostty-vt/src/terminal/PageList.zig | 33 +++++++++++++++++++++-- + vendor/libghostty-vt/src/terminal/c/terminal.zig | 23 ++++++++++++++++ + 2 files changed, 54 insertions(+), 2 deletions(-) + +diff --git a/vendor/libghostty-vt/src/terminal/PageList.zig b/vendor/libghostty-vt/src/terminal/PageList.zig +index 0000000..0000000 100644 +--- a/vendor/libghostty-vt/src/terminal/PageList.zig ++++ b/vendor/libghostty-vt/src/terminal/PageList.zig +@@ -1059,7 +1059,7 @@ fn resizeCols( + break :cursor .{ + .tracked_pin = c.pin orelse try self.trackPin(p), + .untrack = c.pin == null, +- .remaining_rows = self.rows - c.y - 1, ++ .remaining_rows = self.rows -| (c.y + 1), + .wrapped_rows = wrapped, + }; + } else null; +@@ -1192,7 +1192,7 @@ fn resizeCols( + break :wrapped wrapped; + }; + +- const current = self.rows - active_pt.active.y - 1; ++ const current = self.rows -| (active_pt.active.y + 1); + + var req_rows = c.remaining_rows; + req_rows -|= wrapped -| c.wrapped_rows; +@@ -10828,6 +10828,37 @@ test "PageList resize (no reflow) less rows and cols" { + } + } + ++test "PageList resize less rows and cols cursor at bottom" { ++ const testing = std.testing; ++ const alloc = testing.allocator; ++ ++ var s = try init(alloc, 80, 24, 0); ++ defer s.deinit(); ++ ++ const cursor_pin = try s.trackPin(s.pin(.{ .active = .{ ++ .x = 0, ++ .y = s.rows - 1, ++ } }).?); ++ defer s.untrackPin(cursor_pin); ++ ++ // Shrink both axes so the original cursor.y is strictly past the new row ++ // count, after resizeWithoutReflow has already reduced self.rows. ++ try s.resize(.{ ++ .cols = 79, ++ .rows = 20, ++ .reflow = true, ++ .cursor = .{ .x = 0, .y = 23, .pin = cursor_pin }, ++ }); ++ try testing.expectEqual(@as(usize, 79), s.cols); ++ try testing.expectEqual(@as(usize, 20), s.rows); ++ ++ // remaining_rows saturates to 0, so the cursor lands on the new bottom row. ++ try testing.expectEqual(point.Point{ .active = .{ ++ .x = 0, ++ .y = s.rows - 1, ++ } }, s.pointFromPin(.active, cursor_pin.*).?); ++} ++ + test "PageList resize (no reflow) more rows and less cols" { + const testing = std.testing; + const alloc = testing.allocator; +diff --git a/vendor/libghostty-vt/src/terminal/c/terminal.zig b/vendor/libghostty-vt/src/terminal/c/terminal.zig +index 0000000..0000000 100644 +--- a/vendor/libghostty-vt/src/terminal/c/terminal.zig ++++ b/vendor/libghostty-vt/src/terminal/c/terminal.zig +@@ -1005,6 +1005,29 @@ test "resize invalid value" { + try testing.expectEqual(Result.invalid_value, resize(t, 80, 0, 9, 18)); + } + ++test "resize shrinks both axes with cursor at bottom" { ++ var t: Terminal = null; ++ try testing.expectEqual(Result.success, new( ++ &lib.alloc.test_allocator, ++ &t, ++ .{ ++ .cols = 80, ++ .rows = 24, ++ .max_scrollback = 0, ++ }, ++ )); ++ defer free(t); ++ ++ // CSI 24;1H parks the cursor on the bottom row. ++ const move = "\x1b[24;1H"; ++ vt_write(t, move, move.len); ++ ++ // Shrink both axes; pre-resize cursor.y sits past the new bottom row. ++ try testing.expectEqual(Result.success, resize(t, 79, 23, 8, 16)); ++ try testing.expectEqual(79, t.?.terminal.cols); ++ try testing.expectEqual(23, t.?.terminal.rows); ++} ++ + test "mode_get and mode_set" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( +-- +2.49.0 |
