mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 11:46:11 +03:00
terminal/new: Screen.clone
This commit is contained in:
@ -180,6 +180,12 @@ pub fn deinit(self: *PageList) void {
|
||||
}
|
||||
|
||||
/// Clone this pagelist from the top to bottom (inclusive).
|
||||
///
|
||||
/// The viewport is always moved to the top-left.
|
||||
///
|
||||
/// The cloned pagelist must contain at least enough rows for the active
|
||||
/// area. If the region specified has less rows than the active area then
|
||||
/// rows will be added to the bottom of the region to make up the difference.
|
||||
pub fn clone(
|
||||
self: *const PageList,
|
||||
alloc: Allocator,
|
||||
@ -204,20 +210,75 @@ pub fn clone(
|
||||
errdefer page_pool.deinit();
|
||||
|
||||
// Copy our pages
|
||||
const page_list: List = .{};
|
||||
var page_list: List = .{};
|
||||
var total_rows: usize = 0;
|
||||
while (it.next()) |chunk| {
|
||||
_ = chunk;
|
||||
// Clone the page
|
||||
const page = try pool.create();
|
||||
const page_buf = try page_pool.create();
|
||||
page.* = .{ .data = chunk.page.data.cloneBuf(page_buf) };
|
||||
page_list.append(page);
|
||||
|
||||
// If this is a full page then we're done.
|
||||
if (chunk.fullPage()) {
|
||||
total_rows += page.data.size.rows;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this is just a shortened chunk off the end we can just
|
||||
// shorten the size. We don't worry about clearing memory here because
|
||||
// as the page grows the memory will be reclaimable because the data
|
||||
// is still valid.
|
||||
if (chunk.start == 0) {
|
||||
page.data.size.rows = @intCast(chunk.end);
|
||||
total_rows += chunk.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kind of slow, we want to shift the rows up in the page up to
|
||||
// end and then resize down.
|
||||
const rows = page.data.rows.ptr(page.data.memory);
|
||||
const len = chunk.end - chunk.start;
|
||||
for (0..len) |i| {
|
||||
const src: *Row = &rows[i + chunk.start];
|
||||
const dst: *Row = &rows[i];
|
||||
const old_dst = dst.*;
|
||||
dst.* = src.*;
|
||||
src.* = old_dst;
|
||||
}
|
||||
page.data.size.rows = @intCast(len);
|
||||
total_rows += len;
|
||||
}
|
||||
|
||||
return .{
|
||||
var result: PageList = .{
|
||||
.alloc = alloc,
|
||||
.pool = pool,
|
||||
.page_pool = page_pool,
|
||||
.pages = page_list,
|
||||
.page_size = PagePool.item_size * page_count,
|
||||
.max_size = self.max_size,
|
||||
.cols = self.cols,
|
||||
.rows = self.rows,
|
||||
.viewport = .{ .top = {} },
|
||||
};
|
||||
|
||||
// We always need to have enough rows for our viewport because this is
|
||||
// a pagelist invariant that other code relies on.
|
||||
if (total_rows < self.rows) {
|
||||
const len = self.rows - total_rows;
|
||||
for (0..len) |_| {
|
||||
_ = try result.grow();
|
||||
|
||||
// Clear the row. This is not very fast but in reality right
|
||||
// now we rarely clone less than the active area and if we do
|
||||
// the area is by definition very small.
|
||||
const last = result.pages.last.?;
|
||||
const row = &last.data.rows.ptr(last.data.memory)[last.data.size.rows - 1];
|
||||
last.data.clearCells(row, 0, result.cols);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Scroll options.
|
||||
@ -1412,3 +1473,87 @@ test "PageList erase active regrows automatically" {
|
||||
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 10 } });
|
||||
try testing.expect(s.totalRows() == s.rows);
|
||||
}
|
||||
|
||||
test "PageList clone" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 24, null);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
|
||||
var s2 = try s.clone(alloc, .{ .screen = .{} }, null);
|
||||
defer s2.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s2.totalRows());
|
||||
}
|
||||
|
||||
test "PageList clone partial trimmed right" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 20, null);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
try s.growRows(30);
|
||||
|
||||
var s2 = try s.clone(
|
||||
alloc,
|
||||
.{ .screen = .{} },
|
||||
.{ .screen = .{ .y = 39 } },
|
||||
);
|
||||
defer s2.deinit();
|
||||
try testing.expectEqual(@as(usize, 40), s2.totalRows());
|
||||
}
|
||||
|
||||
test "PageList clone partial trimmed left" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 20, null);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
try s.growRows(30);
|
||||
|
||||
var s2 = try s.clone(
|
||||
alloc,
|
||||
.{ .screen = .{ .y = 10 } },
|
||||
null,
|
||||
);
|
||||
defer s2.deinit();
|
||||
try testing.expectEqual(@as(usize, 40), s2.totalRows());
|
||||
}
|
||||
|
||||
test "PageList clone partial trimmed both" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 20, null);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
try s.growRows(30);
|
||||
|
||||
var s2 = try s.clone(
|
||||
alloc,
|
||||
.{ .screen = .{ .y = 10 } },
|
||||
.{ .screen = .{ .y = 35 } },
|
||||
);
|
||||
defer s2.deinit();
|
||||
try testing.expectEqual(@as(usize, 26), s2.totalRows());
|
||||
}
|
||||
|
||||
test "PageList clone less than active" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 24, null);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||
|
||||
var s2 = try s.clone(
|
||||
alloc,
|
||||
.{ .active = .{ .y = 5 } },
|
||||
null,
|
||||
);
|
||||
defer s2.deinit();
|
||||
try testing.expectEqual(@as(usize, s.rows), s2.totalRows());
|
||||
}
|
||||
|
@ -166,8 +166,20 @@ pub fn deinit(self: *Screen) void {
|
||||
///
|
||||
/// - Screen dimensions
|
||||
/// - Screen data (cell state, etc.) for the region
|
||||
/// - Cursor if its in the region. If the cursor is not in the region
|
||||
/// then it will be placed at the top-left of the new screen.
|
||||
///
|
||||
/// Anything not mentioned above is NOT copied. Some of this is for
|
||||
/// very good reason:
|
||||
///
|
||||
/// - Kitty images have a LOT of data. This is not efficient to copy.
|
||||
/// Use a lock and access the image data. The dirty bit is there for
|
||||
/// a reason.
|
||||
/// - Cursor location can be expensive to calculate with respect to the
|
||||
/// specified region. It is faster to grab the cursor from the old
|
||||
/// screen and then move it to the new screen.
|
||||
///
|
||||
/// If not mentioned above, then there isn't a specific reason right now
|
||||
/// to not copy some data other than we probably didn't need it and it
|
||||
/// isn't necessary for screen coherency.
|
||||
///
|
||||
/// Other notes:
|
||||
///
|
||||
@ -180,12 +192,19 @@ pub fn clone(
|
||||
self: *const Screen,
|
||||
alloc: Allocator,
|
||||
top: point.Point,
|
||||
bottom: ?point.Point,
|
||||
bot: ?point.Point,
|
||||
) !Screen {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
_ = top;
|
||||
_ = bottom;
|
||||
var pages = try self.pages.clone(alloc, top, bot);
|
||||
errdefer pages.deinit();
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.pages = pages,
|
||||
.no_scrollback = self.no_scrollback,
|
||||
|
||||
// TODO: let's make this reasonble
|
||||
.cursor = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell {
|
||||
@ -1431,3 +1450,62 @@ test "Screen: scroll and clear ignore blank lines" {
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n3ABCD\nX", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: clone" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 3, 10);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("1ABCD\n2EFGH");
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
|
||||
}
|
||||
|
||||
// Clone
|
||||
var s2 = try s.clone(alloc, .{ .active = .{} }, null);
|
||||
defer s2.deinit();
|
||||
{
|
||||
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
|
||||
}
|
||||
|
||||
// Write to s1, should not be in s2
|
||||
try s.testWriteString("\n34567");
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH\n34567", contents);
|
||||
}
|
||||
{
|
||||
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: clone partial" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 3, 10);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("1ABCD\n2EFGH");
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
|
||||
}
|
||||
|
||||
// Clone
|
||||
var s2 = try s.clone(alloc, .{ .active = .{ .y = 1 } }, null);
|
||||
defer s2.deinit();
|
||||
{
|
||||
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("2EFGH", contents);
|
||||
}
|
||||
}
|
||||
|
@ -266,6 +266,43 @@ pub const Page = struct {
|
||||
@panic("TODO: grapheme move");
|
||||
}
|
||||
|
||||
/// Clear the cells in the given row. This will reclaim memory used
|
||||
/// by graphemes and styles. Note that if the style cleared is still
|
||||
/// active, Page cannot know this and it will still be ref counted down.
|
||||
/// The best solution for this is to artificially increment the ref count
|
||||
/// prior to calling this function.
|
||||
pub fn clearCells(
|
||||
self: *Page,
|
||||
row: *Row,
|
||||
left: usize,
|
||||
end: usize,
|
||||
) void {
|
||||
const cells = row.cells.ptr(self.memory)[left..end];
|
||||
if (row.grapheme) {
|
||||
for (cells) |*cell| {
|
||||
if (cell.hasGrapheme()) self.clearGrapheme(row, cell);
|
||||
}
|
||||
}
|
||||
|
||||
if (row.styled) {
|
||||
for (cells) |*cell| {
|
||||
if (cell.style_id == style.default_id) continue;
|
||||
|
||||
if (self.styles.lookupId(self.memory, cell.style_id)) |prev_style| {
|
||||
// Below upsert can't fail because it should already be present
|
||||
const md = self.styles.upsert(self.memory, prev_style.*) catch unreachable;
|
||||
assert(md.ref > 0);
|
||||
md.ref -= 1;
|
||||
if (md.ref == 0) self.styles.remove(self.memory, cell.style_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (cells.len == self.size.cols) row.styled = false;
|
||||
}
|
||||
|
||||
@memset(cells, .{});
|
||||
}
|
||||
|
||||
/// Append a codepoint to the given cell as a grapheme.
|
||||
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) !void {
|
||||
if (comptime std.debug.runtime_safety) assert(cell.hasText());
|
||||
|
Reference in New Issue
Block a user