terminal: Screen replace adjust with increaseCapacity

This commit is contained in:
Mitchell Hashimoto
2026-01-15 09:07:44 -08:00
parent 1e5973386b
commit 29d4aba033
2 changed files with 72 additions and 31 deletions

View File

@@ -1176,14 +1176,19 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void {
return;
}
// If we have a old style then we need to release it from the old page.
// If we have an old style then we need to release it from the old page.
const old_style_: ?style.Style = if (self.cursor.style_id == style.default_id)
null
else
self.cursor.style;
if (old_style_ != null) {
// Release the style directly from the old page instead of going through
// manualStyleUpdate, because the cursor position may have already been
// updated but the pin has not, which would fail integrity checks.
const old_page: *Page = &self.cursor.page_pin.node.data;
old_page.styles.release(old_page.memory, self.cursor.style_id);
self.cursor.style = .{};
self.manualStyleUpdate() catch unreachable; // Removing a style should never fail
self.cursor.style_id = style.default_id;
}
// If we have a hyperlink then we need to release it from the old page.
@@ -2000,7 +2005,17 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void {
}
/// Call this whenever you manually change the cursor style.
pub fn manualStyleUpdate(self: *Screen) !void {
///
/// Note that this can return any PageList capacity error, because it
/// is possible for the internal pagelist to not accommodate the new style
/// at all. This WILL attempt to resize our internal pages to fit the style
/// but it is possible that it cannot be done, in which case upstream callers
/// need to split the page or do something else.
///
/// NOTE(mitchellh): I think in the future we'll do page splitting
/// automatically here and remove this failure scenario.
pub fn manualStyleUpdate(self: *Screen) PageList.IncreaseCapacityError!void {
defer self.assertIntegrity();
var page: *Page = &self.cursor.page_pin.node.data;
// std.log.warn("active styles={}", .{page.styles.count()});
@@ -2019,6 +2034,9 @@ pub fn manualStyleUpdate(self: *Screen) !void {
// Clear the cursor style ID to prevent weird things from happening
// if the page capacity has to be adjusted which would end up calling
// manualStyleUpdate again.
//
// This also ensures that if anything fails below, we fall back to
// clearing our style.
self.cursor.style_id = style.default_id;
// After setting the style, we need to update our style map.
@@ -2030,30 +2048,50 @@ pub fn manualStyleUpdate(self: *Screen) !void {
page.memory,
self.cursor.style,
) catch |err| id: {
// Our style map is full or needs to be rehashed,
// so we allocate a new page, which will rehash,
// and double the style capacity for it if it was
// full.
const node = try self.adjustCapacity(
// Our style map is full or needs to be rehashed, so we need to
// increase style capacity (or rehash).
const node = try self.increaseCapacity(
self.cursor.page_pin.node,
switch (err) {
error.OutOfMemory => .{ .styles = page.capacity.styles * 2 },
error.NeedsRehash => .{},
error.OutOfMemory => .styles,
error.NeedsRehash => null,
},
);
page = &node.data;
break :id try page.styles.add(
break :id page.styles.add(
page.memory,
self.cursor.style,
);
) catch |err2| switch (err2) {
error.OutOfMemory => {
// This shouldn't happen because increaseCapacity is
// guaranteed to increase our capacity by at least one and
// we only need one space, but again, I don't want to crash
// here so let's log loudly and reset.
log.err("style addition failed after capacity increase", .{});
return error.OutOfMemory;
},
error.NeedsRehash => {
// This should be impossible because we rehash above
// and rehashing should never result in a duplicate. But
// we don't want to simply hard crash so log it and
// clear our style.
log.err("style rehash resulted in needs rehash", .{});
return;
},
};
};
errdefer page.styles.release(page.memory, id);
self.cursor.style_id = id;
self.assertIntegrity();
}
/// Append a grapheme to the given cell within the current cursor row.
pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void {
pub fn appendGrapheme(
self: *Screen,
cell: *Cell,
cp: u21,
) PageList.IncreaseCapacityError!void {
defer self.cursor.page_pin.node.data.assertIntegrity();
self.cursor.page_pin.node.data.appendGrapheme(
self.cursor.page_row,
@@ -2073,11 +2111,9 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void {
// Adjust our capacity. This will update our cursor page pin and
// force us to reload.
const original_node = self.cursor.page_pin.node;
const new_bytes = original_node.data.capacity.grapheme_bytes * 2;
_ = try self.adjustCapacity(
original_node,
.{ .grapheme_bytes = new_bytes },
_ = try self.increaseCapacity(
self.cursor.page_pin.node,
.grapheme_bytes,
);
// The cell pointer is now invalid, so we need to get it from
@@ -2088,17 +2124,22 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void {
.gt => self.cursorCellRight(@intCast(cell_idx - self.cursor.x)),
};
try self.cursor.page_pin.node.data.appendGrapheme(
self.cursor.page_pin.node.data.appendGrapheme(
self.cursor.page_row,
reloaded_cell,
cp,
);
) catch |err2| {
comptime assert(@TypeOf(err2) == error{OutOfMemory});
// This should never happen because we just increased capacity.
// Log loudly but still return an error so we don't just
// crash.
log.err("grapheme append failed after capacity increase", .{});
return err2;
};
},
};
}
pub const StartHyperlinkError = Allocator.Error || PageList.AdjustCapacityError;
/// Start the hyperlink state. Future cells will be marked as hyperlinks with
/// this state. Note that various terminal operations may clear the hyperlink
/// state, such as switching screens (alt screen).
@@ -2106,7 +2147,7 @@ pub fn startHyperlink(
self: *Screen,
uri: []const u8,
id_: ?[]const u8,
) StartHyperlinkError!void {
) PageList.IncreaseCapacityError!void {
// Create our pending entry.
const link: hyperlink.Hyperlink = .{
.uri = uri,
@@ -2131,21 +2172,21 @@ pub fn startHyperlink(
error.OutOfMemory => return error.OutOfMemory,
// strings table is out of memory, adjust it up
error.StringsOutOfMemory => _ = try self.adjustCapacity(
error.StringsOutOfMemory => _ = try self.increaseCapacity(
self.cursor.page_pin.node,
.{ .string_bytes = self.cursor.page_pin.node.data.capacity.string_bytes * 2 },
.string_bytes,
),
// hyperlink set is out of memory, adjust it up
error.SetOutOfMemory => _ = try self.adjustCapacity(
error.SetOutOfMemory => _ = try self.increaseCapacity(
self.cursor.page_pin.node,
.{ .hyperlink_bytes = self.cursor.page_pin.node.data.capacity.hyperlink_bytes * 2 },
.hyperlink_bytes,
),
// hyperlink set is too full, rehash it
error.SetNeedsRehash => _ = try self.adjustCapacity(
error.SetNeedsRehash => _ = try self.increaseCapacity(
self.cursor.page_pin.node,
.{},
null,
),
}

View File

@@ -2038,7 +2038,7 @@ pub const Cell = packed struct(u64) {
test "Page.layout can take a maxed capacity" {
// Our intention is for a maxed-out capacity to always fit
// within a page layout without trigering runtime safety on any
// within a page layout without triggering runtime safety on any
// overflow. This simplifies some of our handling downstream of the
// call (relevant to: https://github.com/ghostty-org/ghostty/issues/10258)
var cap: Capacity = undefined;