mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
terminal/new: PageList.erase
This commit is contained in:
@ -359,6 +359,63 @@ fn createPage(self: *PageList) !*List.Node {
|
||||
return page;
|
||||
}
|
||||
|
||||
/// Erase the rows from the given top to bottom (inclusive). Erasing
|
||||
/// the rows doesn't clear them but actually physically REMOVES the rows.
|
||||
/// If the top or bottom point is in the middle of a page, the other
|
||||
/// contents in the page will be preserved but the page itself will be
|
||||
/// underutilized (size < capacity).
|
||||
pub fn erase(
|
||||
self: *PageList,
|
||||
tl_pt: point.Point,
|
||||
bl_pt: ?point.Point,
|
||||
) void {
|
||||
// A rowChunkIterator iterates one page at a time from the back forward.
|
||||
// "back" here is in terms of scrollback, but actually the front of the
|
||||
// linked list.
|
||||
var it = self.rowChunkIterator(tl_pt, bl_pt);
|
||||
while (it.next()) |chunk| {
|
||||
// If the chunk is a full page, deinit thit page and remove it from
|
||||
// the linked list.
|
||||
if (chunk.fullPage()) {
|
||||
self.erasePage(chunk.page);
|
||||
continue;
|
||||
}
|
||||
|
||||
// The chunk is not a full page so we need to move the rows.
|
||||
// This is a cheap operation because we're just moving cell offsets,
|
||||
// not the actual cell contents.
|
||||
assert(chunk.start == 0);
|
||||
const rows = chunk.page.data.rows.ptr(chunk.page.data.memory);
|
||||
const scroll_amount = chunk.page.data.size.rows - chunk.end;
|
||||
for (0..scroll_amount) |i| {
|
||||
const src: *Row = &rows[i + scroll_amount];
|
||||
const dst: *Row = &rows[i];
|
||||
const old_dst = dst.*;
|
||||
dst.* = src.*;
|
||||
src.* = old_dst;
|
||||
}
|
||||
|
||||
// We don't even bother deleting the data in the swapped rows
|
||||
// because erasing in this way yields a page that likely will never
|
||||
// be written to again (its in the past) or it will grow and the
|
||||
// terminal erase will automatically erase the data.
|
||||
|
||||
chunk.page.data.size.rows = @intCast(scroll_amount);
|
||||
}
|
||||
}
|
||||
|
||||
/// Erase a single page, freeing all its resources. The page can be
|
||||
/// anywhere in the linked list.
|
||||
fn erasePage(self: *PageList, page: *List.Node) void {
|
||||
// Remove the page from the linked list
|
||||
self.pages.remove(page);
|
||||
|
||||
// Reset the page memory and return it back to the pool.
|
||||
@memset(page.data.memory, 0);
|
||||
self.page_pool.destroy(@ptrCast(page.data.memory.ptr));
|
||||
self.pool.destroy(page);
|
||||
}
|
||||
|
||||
/// Get the top-left of the screen for the given tag.
|
||||
pub fn rowOffset(self: *const PageList, pt: point.Point) RowOffset {
|
||||
// TODO: assert the point is valid
|
||||
@ -508,6 +565,11 @@ pub const RowChunkIterator = struct {
|
||||
const rows_ptr = self.page.data.rows.ptr(self.page.data.memory);
|
||||
return rows_ptr[self.start..self.end];
|
||||
}
|
||||
|
||||
/// Returns true if this chunk represents every row in the page.
|
||||
pub fn fullPage(self: Chunk) bool {
|
||||
return self.start == 0 and self.end == self.page.data.size.rows;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -546,7 +608,12 @@ pub fn rowChunkIterator(
|
||||
|
||||
// History goes to the top of the active area. This is more expensive
|
||||
// to calculate but also more rare of a thing to iterate over.
|
||||
.history => .{ .row = self.getTopLeft(.active) },
|
||||
.history => history: {
|
||||
const active_tl = self.getTopLeft(.active);
|
||||
const history_bot = active_tl.backward(1) orelse
|
||||
return .{ .row = null };
|
||||
break :history .{ .row = history_bot };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -1187,7 +1254,28 @@ test "PageList rowChunkIterator history two pages" {
|
||||
try testing.expect(chunk.page == s.pages.first.?);
|
||||
const start: usize = 0;
|
||||
try testing.expectEqual(start, chunk.start);
|
||||
try testing.expectEqual(active_tl.row_offset + 1, chunk.end);
|
||||
try testing.expectEqual(active_tl.row_offset, chunk.end);
|
||||
}
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
test "PageList erase" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 24, null);
|
||||
defer s.deinit();
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
|
||||
// Our total rows should be large
|
||||
try testing.expect(s.totalRows() > s.rows);
|
||||
|
||||
// Erase the entire history, we should be back to just our active set.
|
||||
s.erase(.{ .history = .{} }, null);
|
||||
try testing.expectEqual(s.rows, s.totalRows());
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const ansi = @import("../ansi.zig");
|
||||
const kitty = @import("../kitty.zig");
|
||||
const sgr = @import("../sgr.zig");
|
||||
const unicode = @import("../../unicode/main.zig");
|
||||
const PageList = @import("PageList.zig");
|
||||
@ -35,6 +36,9 @@ saved_cursor: ?SavedCursor = null,
|
||||
/// protection mode since some sequences such as ECH depend on this.
|
||||
protected_mode: ansi.ProtectedMode = .off,
|
||||
|
||||
/// Kitty graphics protocol state.
|
||||
kitty_images: kitty.graphics.ImageStorage = .{},
|
||||
|
||||
/// The cursor position.
|
||||
pub const Cursor = struct {
|
||||
// The x/y position within the viewport.
|
||||
@ -113,6 +117,7 @@ pub fn init(
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Screen) void {
|
||||
self.kitty_images.deinit(self.alloc);
|
||||
self.pages.deinit();
|
||||
}
|
||||
|
||||
@ -270,6 +275,11 @@ pub const Scroll = union(enum) {
|
||||
|
||||
/// Scroll the viewport of the terminal grid.
|
||||
pub fn scroll(self: *Screen, behavior: Scroll) void {
|
||||
// No matter what, scrolling marks our image state as dirty since
|
||||
// it could move placements. If there are no placements or no images
|
||||
// this is still a very cheap operation.
|
||||
self.kitty_images.dirty = true;
|
||||
|
||||
switch (behavior) {
|
||||
.active => self.pages.scroll(.{ .active = {} }),
|
||||
.top => self.pages.scroll(.{ .top = {} }),
|
||||
@ -282,6 +292,11 @@ pub fn scroll(self: *Screen, behavior: Scroll) void {
|
||||
pub fn scrollClear(self: *Screen) !void {
|
||||
try self.pages.scrollClear();
|
||||
self.cursorAbsolute(0, 0);
|
||||
|
||||
// No matter what, scrolling marks our image state as dirty since
|
||||
// it could move placements. If there are no placements or no images
|
||||
// this is still a very cheap operation.
|
||||
self.kitty_images.dirty = true;
|
||||
}
|
||||
|
||||
// Erase the region specified by tl and bl, inclusive. Erased cells are
|
||||
|
Reference in New Issue
Block a user