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).
|
/// 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(
|
pub fn clone(
|
||||||
self: *const PageList,
|
self: *const PageList,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
@ -204,20 +210,75 @@ pub fn clone(
|
|||||||
errdefer page_pool.deinit();
|
errdefer page_pool.deinit();
|
||||||
|
|
||||||
// Copy our pages
|
// Copy our pages
|
||||||
const page_list: List = .{};
|
var page_list: List = .{};
|
||||||
|
var total_rows: usize = 0;
|
||||||
while (it.next()) |chunk| {
|
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,
|
.alloc = alloc,
|
||||||
.pool = pool,
|
.pool = pool,
|
||||||
.page_pool = page_pool,
|
.page_pool = page_pool,
|
||||||
.pages = page_list,
|
.pages = page_list,
|
||||||
|
.page_size = PagePool.item_size * page_count,
|
||||||
.max_size = self.max_size,
|
.max_size = self.max_size,
|
||||||
.cols = self.cols,
|
.cols = self.cols,
|
||||||
.rows = self.rows,
|
.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.
|
/// Scroll options.
|
||||||
@ -1412,3 +1473,87 @@ test "PageList erase active regrows automatically" {
|
|||||||
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 10 } });
|
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 10 } });
|
||||||
try testing.expect(s.totalRows() == s.rows);
|
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 dimensions
|
||||||
/// - Screen data (cell state, etc.) for the region
|
/// - 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:
|
/// Other notes:
|
||||||
///
|
///
|
||||||
@ -180,12 +192,19 @@ pub fn clone(
|
|||||||
self: *const Screen,
|
self: *const Screen,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
top: point.Point,
|
top: point.Point,
|
||||||
bottom: ?point.Point,
|
bot: ?point.Point,
|
||||||
) !Screen {
|
) !Screen {
|
||||||
_ = self;
|
var pages = try self.pages.clone(alloc, top, bot);
|
||||||
_ = alloc;
|
errdefer pages.deinit();
|
||||||
_ = top;
|
|
||||||
_ = bottom;
|
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 {
|
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);
|
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");
|
@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.
|
/// Append a codepoint to the given cell as a grapheme.
|
||||||
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) !void {
|
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) !void {
|
||||||
if (comptime std.debug.runtime_safety) assert(cell.hasText());
|
if (comptime std.debug.runtime_safety) assert(cell.hasText());
|
||||||
|
Reference in New Issue
Block a user