diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 23:05:23 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 23:05:51 +0300 |
| commit | 5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch) | |
| tree | 672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/terminal/PageList.zig | |
| parent | 32abb3776668ee768200019c170956f67fe33a86 (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.zig | 70 |
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; |
