aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/PageList.zig
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:51 +0300
commit5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch)
tree672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/terminal/PageList.zig
parent32abb3776668ee768200019c170956f67fe33a86 (diff)
chore: update libghostty vendor
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/PageList.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/PageList.zig70
1 files changed, 62 insertions, 8 deletions
diff --git a/vendor/libghostty-vt/src/terminal/PageList.zig b/vendor/libghostty-vt/src/terminal/PageList.zig
index 17eea73d..8e5cd193 100644
--- a/vendor/libghostty-vt/src/terminal/PageList.zig
+++ b/vendor/libghostty-vt/src/terminal/PageList.zig
@@ -658,6 +658,11 @@ pub fn deinit(self: *PageList) void {
pub fn reset(self: *PageList) void {
defer self.assertIntegrity();
+ // Invalidate all external page refs to the previous list. The reset below
+ // rebuilds the page list from the pools, so old untracked refs must be
+ // rejected before any validation attempts to inspect their node pointers.
+ self.page_serial_min = self.page_serial;
+
// We need enough pages/nodes to keep our active area. This should
// never fail since we by definition have allocated a page already
// that fits our size but I'm not confident to make that assertion.
@@ -935,6 +940,10 @@ pub const Resize = struct {
pub const Cursor = struct {
x: size.CellCountInt,
y: size.CellCountInt,
+
+ /// When set, this pin preserves right-side blank cells up to the cursor
+ /// during reflow.
+ pin: ?*Pin = null,
};
};
@@ -1013,10 +1022,6 @@ fn resizeCols(
) Allocator.Error!void {
assert(cols != self.cols);
- // Update our cols. We have to do this early because grow() that we
- // may call below relies on this to calculate the proper page size.
- self.cols = cols;
-
// If we have a cursor position (x,y), then we try under any col resizing
// to keep the same number remaining active rows beneath it. This is a
// very special case if you can imagine clearing the screen (i.e.
@@ -1025,10 +1030,11 @@ fn resizeCols(
// pull down scrollback.
const preserved_cursor: ?struct {
tracked_pin: *Pin,
+ untrack: bool,
remaining_rows: usize,
wrapped_rows: usize,
} = if (cursor) |c| cursor: {
- const p = self.pin(.{ .active = .{
+ const p = if (c.pin) |cursor_pin| cursor_pin.* else self.pin(.{ .active = .{
.x = c.x,
.y = c.y,
} }) orelse break :cursor null;
@@ -1051,12 +1057,21 @@ fn resizeCols(
};
break :cursor .{
- .tracked_pin = try self.trackPin(p),
+ .tracked_pin = c.pin orelse try self.trackPin(p),
+ .untrack = c.pin == null,
.remaining_rows = self.rows - c.y - 1,
.wrapped_rows = wrapped,
};
} else null;
- defer if (preserved_cursor) |c| self.untrackPin(c.tracked_pin);
+ defer if (preserved_cursor) |c| {
+ if (c.untrack) self.untrackPin(c.tracked_pin);
+ };
+
+ // Update our cols. We have to do this early because grow() that we
+ // may call below relies on this to calculate the proper page size, but
+ // after preserved_cursor so that the cursor pin can resolve coordinates in
+ // the old active coordinate space.
+ self.cols = cols;
// Create the first node that contains our reflow.
const first_rewritten_node = node: {
@@ -1110,7 +1125,11 @@ fn resizeCols(
{
var reflow_cursor: ReflowCursor = .init(first_rewritten_node);
while (it.next()) |row| {
- try reflow_cursor.reflowRow(self, row);
+ try reflow_cursor.reflowRow(
+ self,
+ row,
+ if (preserved_cursor) |c| c.tracked_pin else null,
+ );
// Once we're done reflowing a page, destroy it immediately.
// This frees memory and makes it more likely in memory
@@ -1226,6 +1245,7 @@ const ReflowCursor = struct {
self: *ReflowCursor,
list: *PageList,
row: Pin,
+ cursor_pin: ?*Pin,
) Allocator.Error!void {
const src_page: *Page = &row.node.data;
const src_row = row.rowAndCell().row;
@@ -1253,6 +1273,8 @@ const ReflowCursor = struct {
if (&p.node.data != src_page or
p.y != src_y) continue;
+ if (cursor_pin != null and p == cursor_pin.?) continue;
+
// If this pin is in the blanks on the right and past the end
// of the dst col width then we move it to the end of the dst
// col width instead.
@@ -1268,6 +1290,14 @@ const ReflowCursor = struct {
}
}
+ // If the cursor is after blanks on the right, those cells are still
+ // before the next write and must reflow with it.
+ if (cursor_pin) |p| {
+ if (&p.node.data == src_page and p.y == src_y) {
+ cols_len = @max(cols_len, p.x + 1);
+ }
+ }
+
// Defer processing of blank rows so that blank rows
// at the end of the page list are never written.
if (cols_len == 0) {
@@ -13518,6 +13548,30 @@ test "PageList reset" {
}, s.getTopLeft(.active));
}
+test "PageList reset invalidates stale untracked refs even if node memory is reused" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, null);
+ defer s.deinit();
+
+ const old_serial = s.pages.first.?.serial;
+ try testing.expect(old_serial >= s.page_serial_min);
+ try testing.expect(old_serial < s.page_serial);
+
+ s.reset();
+
+ // The important safety property is that stale serials are rejected before
+ // the node pointer is inspected. Reset rebuilds the page list from the
+ // pools, so old untracked refs may contain node pointers that are no
+ // longer safe to dereference.
+ try testing.expect(old_serial < s.page_serial_min);
+
+ const new_serial = s.pages.first.?.serial;
+ try testing.expect(new_serial >= s.page_serial_min);
+ try testing.expect(new_serial < s.page_serial);
+}
+
test "PageList reset across two pages" {
const testing = std.testing;
const alloc = testing.allocator;