terminal: reflowRow extract writeCell

This commit is contained in:
Mitchell Hashimoto
2026-01-16 06:22:05 -08:00
parent d626984418
commit 25643ec806

View File

@@ -1247,355 +1247,19 @@ const ReflowCursor = struct {
}
}
const cell = &cells[x];
x += 1;
// Copy cell contents.
switch (cell.content_tag) {
.codepoint,
.codepoint_grapheme,
=> switch (cell.wide) {
.narrow => self.page_cell.* = cell.*,
.wide => if (self.page.size.cols > 1) {
if (self.x == self.page.size.cols - 1) {
// If there's a wide character in the last column of
// the reflowed page then we need to insert a spacer
// head and wrap before handling it.
self.page_cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 0 },
.wide = .spacer_head,
};
// Decrement the source position so that when we
// loop we'll process this source cell again,
// since we can't copy it into a spacer head.
x -= 1;
// Move to the next row (this sets pending wrap
// which will cause us to wrap on the next
// iteration).
self.cursorForward();
continue;
} else {
self.page_cell.* = cell.*;
}
} else {
// Edge case, when resizing to 1 column, wide
// characters are just destroyed and replaced
// with empty narrow cells.
self.page_cell.content.codepoint = 0;
self.page_cell.wide = .narrow;
self.cursorForward();
// Skip spacer tail so it doesn't cause a wrap.
x += 1;
continue;
},
.spacer_tail => if (self.page.size.cols > 1) {
self.page_cell.* = cell.*;
} else {
// Edge case, when resizing to 1 column, wide
// characters are just destroyed and replaced
// with empty narrow cells, so we should just
// discard any spacer tails.
continue;
},
.spacer_head => {
// Spacer heads should be ignored. If we need a
// spacer head in our reflowed page, it is added
// when processing the wide cell it belongs to.
continue;
},
},
.bg_color_palette,
.bg_color_rgb,
=> {
// These are guaranteed to have no style or grapheme
// data associated with them so we can fast path them.
self.page_cell.* = cell.*;
self.cursorForward();
continue;
},
switch (try self.writeCell(
list,
&cells[x],
src_page,
)) {
// Wrote the cell, move to the next.
.success => x += 1,
// Wrote the cell but request to skip the next so skip it.
// This is used for things like spacers.
.skip_next => x += 2,
// Didn't write the cell, repeat writing this same cell.
.repeat => {},
}
// These will create issues by trying to clone managed memory that
// isn't set if the current dst row needs to be moved to a new page.
// They'll be fixed once we do properly copy the relevant memory.
self.page_cell.content_tag = .codepoint;
self.page_cell.hyperlink = false;
self.page_cell.style_id = stylepkg.default_id;
// std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{
// src_y,
// x,
// self.y,
// self.x,
// self.page.size.cols,
// cell.content.codepoint,
// cell.wide,
// self.page_cell.wide,
// });
// Copy grapheme data.
if (cell.content_tag == .codepoint_grapheme) {
// Copy the graphemes
const cps = src_page.lookupGrapheme(cell).?;
// If our page can't support an additional cell
// with graphemes then we increase capacity.
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
try self.increaseCapacity(
list,
.grapheme_bytes,
);
}
// Attempt to allocate the space that would be required
// for these graphemes, and if it's not available, then
// increase capacity. Keep trying until we succeed.
while (true) {
if (self.page.grapheme_alloc.alloc(
u21,
self.page.memory,
cps.len,
)) |slice| {
self.page.grapheme_alloc.free(
self.page.memory,
slice,
);
break;
} else |_| {
// Grow our capacity until we can fit the extra bytes.
try self.increaseCapacity(list, .grapheme_bytes);
}
}
self.page.setGraphemes(
self.page_row,
self.page_cell,
cps,
) catch |err| {
// This shouldn't fail since we made sure we have space
// above. There is no reasonable behavior we can take here
// so we have a warn level log. This is ALMOST non-recoverable,
// though we choose to recover by corrupting the cell
// to a non-grapheme codepoint.
log.err("setGraphemes failed after capacity increase err={}", .{err});
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
// Unsafe builds we throw away grapheme data!
cell.content_tag = .codepoint;
cell.content = .{ .codepoint = 0xFFFD };
};
}
// Copy hyperlink data.
if (cell.hyperlink) hyperlink: {
const src_id = src_page.lookupHyperlink(cell).?;
const src_link = src_page.hyperlink_set.get(src_page.memory, src_id);
// If our page can't support an additional cell
// with a hyperlink then we increase capacity.
if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) {
try self.increaseCapacity(list, .hyperlink_bytes);
}
// Ensure that the string alloc has sufficient capacity
// to dupe the link (and the ID if it's not implicit).
const additional_required_string_capacity =
src_link.uri.len +
switch (src_link.id) {
.explicit => |v| v.len,
.implicit => 0,
};
// Keep trying until we have enough capacity.
while (true) {
if (self.page.string_alloc.alloc(
u8,
self.page.memory,
additional_required_string_capacity,
)) |slice| {
// We have enough capacity, free the test alloc.
self.page.string_alloc.free(
self.page.memory,
slice,
);
break;
} else |_| {
// Grow our capacity until we can fit the extra bytes.
try self.increaseCapacity(
list,
.string_bytes,
);
}
}
const dst_link = src_link.dupe(
src_page,
self.page,
) catch |err| {
// This shouldn't fail since we did a capacity
// check above.
log.err("link dupe failed with capacity check err={}", .{err});
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
cell.hyperlink = false;
break :hyperlink;
};
const dst_id = self.page.hyperlink_set.addWithIdContext(
self.page.memory,
dst_link,
src_id,
.{ .page = self.page },
) catch |err| id: {
// If the add failed then either the set needs to grow
// or it needs to be rehashed. Either one of those can
// be accomplished by increasing capacity, either with
// no actual change or with an increased hyperlink cap.
try self.increaseCapacity(list, switch (err) {
error.OutOfMemory => .hyperlink_bytes,
error.NeedsRehash => null,
});
// We dupe the link again, and don't have to worry about
// freeing the other one because increasing the capacity
// destroyed the prior page.
const dst_link2 = src_link.dupe(
src_page,
self.page,
) catch |err2| {
// This shouldn't fail since we did a capacity
// check above.
log.err("link dupe failed with capacity check err={}", .{err2});
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
cell.hyperlink = false;
break :hyperlink;
};
// We assume this one will succeed. We dupe the link
// again, and don't have to worry about the other one
// because increasing the capacity naturally clears up
// any managed memory not associated with a cell yet.
break :id self.page.hyperlink_set.addWithIdContext(
self.page.memory,
dst_link2,
src_id,
.{ .page = self.page },
) catch |err2| {
// This shouldn't happen since we increased capacity
// above so we handle it like the other similar
// cases and log it, crash in safe builds, and
// remove the hyperlink in unsafe builds.
log.err(
"addWithIdContext failed after capacity increase err={}",
.{err2},
);
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
dst_link2.free(self.page);
cell.hyperlink = false;
break :hyperlink;
};
} orelse src_id;
// We expect this to succeed due to the hyperlinkCapacity
// check we did before. If it doesn't succeed let's
// log it, crash (in safe builds), and clear our state.
self.page.setHyperlink(
self.page_row,
self.page_cell,
dst_id,
) catch |err| {
log.err(
"setHyperlink failed after capacity increase err={}",
.{err},
);
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
// Unsafe builds we throw away hyperlink data!
self.page.hyperlink_set.release(self.page.memory, dst_id);
cell.hyperlink = false;
break :hyperlink;
};
}
// Copy style data.
if (cell.hasStyling()) style: {
const style = src_page.styles.get(
src_page.memory,
cell.style_id,
).*;
const id = self.page.styles.addWithId(
self.page.memory,
style,
cell.style_id,
) catch |err| id: {
// If the add failed then either the set needs to grow
// or it needs to be rehashed. Either one of those can
// be accomplished by increasing capacity, either with
// no actual change or with an increased style cap.
try self.increaseCapacity(list, switch (err) {
error.OutOfMemory => .styles,
error.NeedsRehash => null,
});
// We assume this one will succeed.
break :id self.page.styles.addWithId(
self.page.memory,
style,
cell.style_id,
) catch |err2| {
// Should not fail since we just modified capacity
// above. Log it, crash in safe builds, clear style
// in unsafe builds.
log.err(
"addWithId failed after capacity increase err={}",
.{err2},
);
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
cell.style_id = stylepkg.default_id;
break :style;
};
} orelse cell.style_id;
self.page_row.styled = true;
self.page_cell.style_id = id;
}
if (comptime build_options.kitty_graphics) {
// Copy Kitty virtual placeholder status
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
self.page_row.kitty_virtual_placeholder = true;
}
}
self.cursorForward();
}
// If the source row isn't wrapped then we should scroll afterwards.
@@ -1604,6 +1268,377 @@ const ReflowCursor = struct {
}
}
/// Write a cell. On error, this will not unwrite the cell but
/// the cell may be incomplete (but valid). For example, if the source
/// cell is styled and we failed to allocate space for styles, the
/// written cell may not be styled but it is valid.
///
/// The key failure to recognize for callers is when we can't increase
/// capacity in our destination page. In this case, the caller may want
/// to split the page at this row, rewrite the row into a new page
/// and continue from there.
///
/// But this function guarantees the terminal/page will be in a
/// coherent state even on error.
fn writeCell(
self: *ReflowCursor,
list: *PageList,
cell: *const pagepkg.Cell,
src_page: *const Page,
) IncreaseCapacityError!enum {
success,
repeat,
skip_next,
} {
// Copy cell contents.
switch (cell.content_tag) {
.codepoint,
.codepoint_grapheme,
=> switch (cell.wide) {
.narrow => self.page_cell.* = cell.*,
.wide => if (self.page.size.cols > 1) {
if (self.x == self.page.size.cols - 1) {
// If there's a wide character in the last column of
// the reflowed page then we need to insert a spacer
// head and wrap before handling it.
self.page_cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 0 },
.wide = .spacer_head,
};
// Move to the next row (this sets pending wrap
// which will cause us to wrap on the next
// iteration).
self.cursorForward();
// Decrement the source position so that when we
// loop we'll process this source cell again,
// since we can't copy it into a spacer head.
return .repeat;
} else {
self.page_cell.* = cell.*;
}
} else {
// Edge case, when resizing to 1 column, wide
// characters are just destroyed and replaced
// with empty narrow cells.
self.page_cell.content.codepoint = 0;
self.page_cell.wide = .narrow;
self.cursorForward();
// Skip spacer tail so it doesn't cause a wrap.
return .skip_next;
},
.spacer_tail => if (self.page.size.cols > 1) {
self.page_cell.* = cell.*;
} else {
// Edge case, when resizing to 1 column, wide
// characters are just destroyed and replaced
// with empty narrow cells, so we should just
// discard any spacer tails.
return .success;
},
.spacer_head => {
// Spacer heads should be ignored. If we need a
// spacer head in our reflowed page, it is added
// when processing the wide cell it belongs to.
return .success;
},
},
.bg_color_palette,
.bg_color_rgb,
=> {
// These are guaranteed to have no style or grapheme
// data associated with them so we can fast path them.
self.page_cell.* = cell.*;
self.cursorForward();
return .success;
},
}
// These will create issues by trying to clone managed memory that
// isn't set if the current dst row needs to be moved to a new page.
// They'll be fixed once we do properly copy the relevant memory.
self.page_cell.content_tag = .codepoint;
self.page_cell.hyperlink = false;
self.page_cell.style_id = stylepkg.default_id;
// std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{
// src_y,
// x,
// self.y,
// self.x,
// self.page.size.cols,
// cell.content.codepoint,
// cell.wide,
// self.page_cell.wide,
// });
if (comptime build_options.kitty_graphics) {
// Copy Kitty virtual placeholder status
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
self.page_row.kitty_virtual_placeholder = true;
}
}
// Copy grapheme data.
if (cell.content_tag == .codepoint_grapheme) {
// Copy the graphemes
const cps = src_page.lookupGrapheme(cell).?;
// If our page can't support an additional cell
// with graphemes then we increase capacity.
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
try self.increaseCapacity(
list,
.grapheme_bytes,
);
}
// Attempt to allocate the space that would be required
// for these graphemes, and if it's not available, then
// increase capacity. Keep trying until we succeed.
while (true) {
if (self.page.grapheme_alloc.alloc(
u21,
self.page.memory,
cps.len,
)) |slice| {
self.page.grapheme_alloc.free(
self.page.memory,
slice,
);
break;
} else |_| {
// Grow our capacity until we can fit the extra bytes.
try self.increaseCapacity(list, .grapheme_bytes);
}
}
self.page.setGraphemes(
self.page_row,
self.page_cell,
cps,
) catch |err| {
// This shouldn't fail since we made sure we have space
// above. There is no reasonable behavior we can take here
// so we have a warn level log. This is ALMOST non-recoverable,
// though we choose to recover by corrupting the cell
// to a non-grapheme codepoint.
log.err("setGraphemes failed after capacity increase err={}", .{err});
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
// Unsafe builds we throw away grapheme data!
cell.content_tag = .codepoint;
cell.content = .{ .codepoint = 0xFFFD };
};
}
// Copy hyperlink data.
if (cell.hyperlink) hyperlink: {
const src_id = src_page.lookupHyperlink(cell).?;
const src_link = src_page.hyperlink_set.get(src_page.memory, src_id);
// If our page can't support an additional cell
// with a hyperlink then we increase capacity.
if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) {
try self.increaseCapacity(list, .hyperlink_bytes);
}
// Ensure that the string alloc has sufficient capacity
// to dupe the link (and the ID if it's not implicit).
const additional_required_string_capacity =
src_link.uri.len +
switch (src_link.id) {
.explicit => |v| v.len,
.implicit => 0,
};
// Keep trying until we have enough capacity.
while (true) {
if (self.page.string_alloc.alloc(
u8,
self.page.memory,
additional_required_string_capacity,
)) |slice| {
// We have enough capacity, free the test alloc.
self.page.string_alloc.free(
self.page.memory,
slice,
);
break;
} else |_| {
// Grow our capacity until we can fit the extra bytes.
try self.increaseCapacity(
list,
.string_bytes,
);
}
}
const dst_link = src_link.dupe(
src_page,
self.page,
) catch |err| {
// This shouldn't fail since we did a capacity
// check above.
log.err("link dupe failed with capacity check err={}", .{err});
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
break :hyperlink;
};
const dst_id = self.page.hyperlink_set.addWithIdContext(
self.page.memory,
dst_link,
src_id,
.{ .page = self.page },
) catch |err| id: {
// Always free our original link in case the increaseCap
// call fails so we aren't leaking memory.
dst_link.free(self.page);
// If the add failed then either the set needs to grow
// or it needs to be rehashed. Either one of those can
// be accomplished by increasing capacity, either with
// no actual change or with an increased hyperlink cap.
try self.increaseCapacity(list, switch (err) {
error.OutOfMemory => .hyperlink_bytes,
error.NeedsRehash => null,
});
// We need to recreate the link into the new page.
const dst_link2 = src_link.dupe(
src_page,
self.page,
) catch |err2| {
// This shouldn't fail since we did a capacity
// check above.
log.err("link dupe failed with capacity check err={}", .{err2});
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
cell.hyperlink = false;
break :hyperlink;
};
// We assume this one will succeed. We dupe the link
// again, and don't have to worry about the other one
// because increasing the capacity naturally clears up
// any managed memory not associated with a cell yet.
break :id self.page.hyperlink_set.addWithIdContext(
self.page.memory,
dst_link2,
src_id,
.{ .page = self.page },
) catch |err2| {
// This shouldn't happen since we increased capacity
// above so we handle it like the other similar
// cases and log it, crash in safe builds, and
// remove the hyperlink in unsafe builds.
log.err(
"addWithIdContext failed after capacity increase err={}",
.{err2},
);
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
dst_link2.free(self.page);
cell.hyperlink = false;
break :hyperlink;
};
} orelse src_id;
// We expect this to succeed due to the hyperlinkCapacity
// check we did before. If it doesn't succeed let's
// log it, crash (in safe builds), and clear our state.
self.page.setHyperlink(
self.page_row,
self.page_cell,
dst_id,
) catch |err| {
log.err(
"setHyperlink failed after capacity increase err={}",
.{err},
);
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
// Unsafe builds we throw away hyperlink data!
self.page.hyperlink_set.release(self.page.memory, dst_id);
cell.hyperlink = false;
break :hyperlink;
};
}
// Copy style data.
if (cell.hasStyling()) style: {
const style = src_page.styles.get(
src_page.memory,
cell.style_id,
).*;
const id = self.page.styles.addWithId(
self.page.memory,
style,
cell.style_id,
) catch |err| id: {
// If the add failed then either the set needs to grow
// or it needs to be rehashed. Either one of those can
// be accomplished by increasing capacity, either with
// no actual change or with an increased style cap.
try self.increaseCapacity(list, switch (err) {
error.OutOfMemory => .styles,
error.NeedsRehash => null,
});
// We assume this one will succeed.
break :id self.page.styles.addWithId(
self.page.memory,
style,
cell.style_id,
) catch |err2| {
// Should not fail since we just modified capacity
// above. Log it, crash in safe builds, clear style
// in unsafe builds.
log.err(
"addWithId failed after capacity increase err={}",
.{err2},
);
if (comptime std.debug.runtime_safety) {
// Force a crash with safe builds.
unreachable;
}
cell.style_id = stylepkg.default_id;
break :style;
};
} orelse cell.style_id;
self.page_row.styled = true;
self.page_cell.style_id = id;
}
self.cursorForward();
return .success;
}
/// Create a new page in the provided list with the provided
/// capacity then clone the row currently being worked on to
/// it and delete it from the old page. Places cursor in the
@@ -1654,7 +1689,7 @@ const ReflowCursor = struct {
self: *ReflowCursor,
list: *PageList,
adjustment: ?IncreaseCapacity,
) !void {
) IncreaseCapacityError!void {
const old_x = self.x;
const old_y = self.y;
const old_total_rows = self.total_rows;