mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal/new: lots of code thrown at the wall
This commit is contained in:
@ -55,6 +55,7 @@ test {
|
||||
_ = @import("new/page.zig");
|
||||
_ = @import("new/PageList.zig");
|
||||
_ = @import("new/Screen.zig");
|
||||
_ = @import("new/point.zig");
|
||||
_ = @import("new/size.zig");
|
||||
_ = @import("new/style.zig");
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ const PageList = @This();
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const point = @import("point.zig");
|
||||
const pagepkg = @import("page.zig");
|
||||
const Page = pagepkg.Page;
|
||||
|
||||
@ -41,10 +42,15 @@ pool: Pool,
|
||||
/// The list of pages in the screen.
|
||||
pages: List,
|
||||
|
||||
/// The page that contains the top of the current viewport and the row
|
||||
/// within that page that is the top of the viewport (0-indexed).
|
||||
viewport: *List.Node,
|
||||
viewport_row: usize,
|
||||
/// The top-left of certain parts of the screen that are frequently
|
||||
/// accessed so we don't have to traverse the linked list to find them.
|
||||
///
|
||||
/// For other tags, don't need this:
|
||||
/// - screen: pages.first
|
||||
/// - history: active row minus one
|
||||
///
|
||||
viewport: RowOffset,
|
||||
active: RowOffset,
|
||||
|
||||
/// The current desired screen dimensions. I say "desired" because individual
|
||||
/// pages may still be a different size and not yet reflowed since we lazily
|
||||
@ -87,8 +93,8 @@ pub fn init(
|
||||
.rows = rows,
|
||||
.pool = pool,
|
||||
.pages = page_list,
|
||||
.viewport = page,
|
||||
.viewport_row = 0,
|
||||
.viewport = .{ .page = page },
|
||||
.active = .{ .page = page },
|
||||
};
|
||||
}
|
||||
|
||||
@ -99,6 +105,125 @@ pub fn deinit(self: *PageList) void {
|
||||
self.pool.deinit();
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
// This should never return null because we assert the point is valid.
|
||||
return (switch (pt) {
|
||||
.active => |v| self.active.forward(v.y),
|
||||
.viewport => |v| self.viewport.forward(v.y),
|
||||
.screen, .history => |v| offset: {
|
||||
const tl: RowOffset = .{ .page = self.pages.first.? };
|
||||
break :offset tl.forward(v.y);
|
||||
},
|
||||
}).?;
|
||||
}
|
||||
|
||||
/// Get the cell at the given point, or null if the cell does not
|
||||
/// exist or is out of bounds.
|
||||
pub fn getCell(self: *const PageList, pt: point.Point) ?Cell {
|
||||
const row = self.getTopLeft(pt).forward(pt.y) orelse return null;
|
||||
const rac = row.page.data.getRowAndCell(row.row_offset, pt.x);
|
||||
return .{
|
||||
.page = row.page,
|
||||
.row = rac.row,
|
||||
.cell = rac.cell,
|
||||
.row_idx = row.row_offset,
|
||||
.col_idx = pt.x,
|
||||
};
|
||||
}
|
||||
|
||||
pub const RowIterator = struct {
|
||||
row: ?RowOffset = null,
|
||||
limit: ?usize = null,
|
||||
|
||||
pub fn next(self: *RowIterator) ?RowOffset {
|
||||
const row = self.row orelse return null;
|
||||
self.row = row.forward(1);
|
||||
if (self.limit) |*limit| {
|
||||
limit.* -= 1;
|
||||
if (limit.* == 0) self.row = null;
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
};
|
||||
|
||||
/// Create an interator that can be used to iterate all the rows in
|
||||
/// a region of the screen from the given top-left. The tag of the
|
||||
/// top-left point will also determine the end of the iteration,
|
||||
/// so convert from one reference point to another to change the
|
||||
/// iteration bounds.
|
||||
pub fn rowIterator(
|
||||
self: *const PageList,
|
||||
tl_pt: point.Point,
|
||||
) RowIterator {
|
||||
const tl = self.getTopLeft(tl_pt);
|
||||
|
||||
// TODO: limits
|
||||
return .{ .row = tl.forward(tl_pt.coord().y) };
|
||||
}
|
||||
|
||||
/// Get the top-left of the screen for the given tag.
|
||||
fn getTopLeft(self: *const PageList, tag: point.Tag) RowOffset {
|
||||
return switch (tag) {
|
||||
.active => self.active,
|
||||
.viewport => self.viewport,
|
||||
.screen, .history => .{ .page = self.pages.first.? },
|
||||
};
|
||||
}
|
||||
|
||||
/// Represents some y coordinate within the screen. Since pages can
|
||||
/// be split at any row boundary, getting some Y-coordinate within
|
||||
/// any part of the screen may map to a different page and row offset
|
||||
/// than the original y-coordinate. This struct represents that mapping.
|
||||
pub const RowOffset = struct {
|
||||
page: *List.Node,
|
||||
row_offset: usize = 0,
|
||||
|
||||
pub fn rowAndCell(self: RowOffset, x: usize) struct {
|
||||
row: *pagepkg.Row,
|
||||
cell: *pagepkg.Cell,
|
||||
} {
|
||||
const rac = self.page.data.getRowAndCell(x, self.row_offset);
|
||||
return .{ .row = rac.row, .cell = rac.cell };
|
||||
}
|
||||
|
||||
/// Get the row at the given row index from this Topleft. This
|
||||
/// may require traversing into the next page if the row index
|
||||
/// is greater than the number of rows in this page.
|
||||
///
|
||||
/// This will return null if the row index is out of bounds.
|
||||
fn forward(self: RowOffset, idx: usize) ?RowOffset {
|
||||
// Index fits within this page
|
||||
var rows = self.page.data.capacity.rows - self.row_offset;
|
||||
if (idx < rows) return .{
|
||||
.page = self.page,
|
||||
.row_offset = idx + self.row_offset,
|
||||
};
|
||||
|
||||
// Need to traverse page links to find the page
|
||||
var page: *List.Node = self.page;
|
||||
var idx_left: usize = idx;
|
||||
while (idx_left >= rows) {
|
||||
idx_left -= rows;
|
||||
page = page.next orelse return null;
|
||||
rows = page.data.capacity.rows;
|
||||
}
|
||||
|
||||
return .{ .page = page, .row_offset = idx_left };
|
||||
}
|
||||
};
|
||||
|
||||
const Cell = struct {
|
||||
page: *List.Node,
|
||||
row: *pagepkg.Row,
|
||||
cell: *pagepkg.Cell,
|
||||
row_idx: usize,
|
||||
col_idx: usize,
|
||||
};
|
||||
|
||||
test "PageList" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -4,58 +4,21 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const unicode = @import("../../unicode/main.zig");
|
||||
const PageList = @import("PageList.zig");
|
||||
const pagepkg = @import("page.zig");
|
||||
const point = @import("point.zig");
|
||||
const Page = pagepkg.Page;
|
||||
|
||||
// Some magic constants we use that could be tweaked...
|
||||
|
||||
/// The number of PageList.Nodes we preheat the pool with. A node is
|
||||
/// a very small struct so we can afford to preheat many, but the exact
|
||||
/// number is uncertain. Any number too large is wasting memory, any number
|
||||
/// too small will cause the pool to have to allocate more memory later.
|
||||
/// This should be set to some reasonable minimum that we expect a terminal
|
||||
/// window to scroll into quickly.
|
||||
const page_preheat = 4;
|
||||
|
||||
/// The default number of unique styles per page we expect. It is currently
|
||||
/// "32" because anecdotally amongst a handful of beta testers, no one
|
||||
/// under normal terminal use ever used more than 32 unique styles in a
|
||||
/// single page. We found outliers but it was rare enough that we could
|
||||
/// allocate those when necessary.
|
||||
const page_default_styles = 32;
|
||||
|
||||
/// The list of pages in the screen. These are expected to be in order
|
||||
/// where the first page is the topmost page (scrollback) and the last is
|
||||
/// the bottommost page (the current active page).
|
||||
const PageList = std.DoublyLinkedList(Page);
|
||||
|
||||
/// The memory pool we get page nodes from.
|
||||
const PagePool = std.heap.MemoryPool(PageList.Node);
|
||||
|
||||
/// The general purpose allocator to use for all memory allocations.
|
||||
/// Unfortunately some screen operations do require allocation.
|
||||
alloc: Allocator,
|
||||
|
||||
/// The memory pool we get page nodes for the linked list from.
|
||||
page_pool: PagePool,
|
||||
|
||||
/// The list of pages in the screen.
|
||||
pages: PageList,
|
||||
|
||||
/// The page that contains the top of the current viewport and the row
|
||||
/// within that page that is the top of the viewport (0-indexed).
|
||||
viewport: *PageList.Node,
|
||||
viewport_row: usize,
|
||||
|
||||
/// The current cursor position
|
||||
cursor: Cursor,
|
||||
|
||||
/// The current desired screen dimensions. I say "desired" because individual
|
||||
/// pages may still be a different size and not yet reflowed since we lazily
|
||||
/// reflow text.
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
|
||||
/// The cursor position.
|
||||
const Cursor = struct {
|
||||
// The x/y position within the viewport.
|
||||
@ -66,12 +29,11 @@ const Cursor = struct {
|
||||
/// next character print will force a soft-wrap.
|
||||
pending_wrap: bool = false,
|
||||
|
||||
// The page that the cursor is on and the offset into that page that
|
||||
// the current y exists.
|
||||
page: *PageList.Node,
|
||||
page_row: usize,
|
||||
page_row_ptr: *pagepkg.Row,
|
||||
page_cell_ptr: *pagepkg.Cell,
|
||||
/// The pointers into the page list where the cursor is currently
|
||||
/// located. This makes it faster to move the cursor.
|
||||
page_offset: PageList.RowOffset,
|
||||
page_row: *pagepkg.Row,
|
||||
page_cell: *pagepkg.Cell,
|
||||
};
|
||||
|
||||
/// Initialize a new screen.
|
||||
@ -81,65 +43,82 @@ pub fn init(
|
||||
rows: usize,
|
||||
max_scrollback: usize,
|
||||
) !Screen {
|
||||
_ = max_scrollback;
|
||||
// Initialize our backing pages. This will initialize the viewport.
|
||||
var pages = try PageList.init(alloc, cols, rows, max_scrollback);
|
||||
errdefer pages.deinit();
|
||||
|
||||
// The screen starts with a single page that is the entire viewport,
|
||||
// and we'll split it thereafter if it gets too large and add more as
|
||||
// necessary.
|
||||
var pool = try PagePool.initPreheated(alloc, page_preheat);
|
||||
errdefer pool.deinit();
|
||||
|
||||
var page = try pool.create();
|
||||
// no errdefer because the pool deinit will clean up the page
|
||||
|
||||
page.* = .{
|
||||
.data = try Page.init(alloc, .{
|
||||
.cols = cols,
|
||||
.rows = rows,
|
||||
.styles = page_default_styles,
|
||||
}),
|
||||
};
|
||||
errdefer page.data.deinit(alloc);
|
||||
|
||||
var page_list: PageList = .{};
|
||||
page_list.prepend(page);
|
||||
|
||||
const cursor_row_ptr, const cursor_cell_ptr = ptr: {
|
||||
const rac = page.data.getRowAndCell(0, 0);
|
||||
break :ptr .{ rac.row, rac.cell };
|
||||
};
|
||||
// The viewport is guaranteed to exist, so grab it so we can setup
|
||||
// our initial cursor.
|
||||
const page_offset = pages.rowOffset(.{ .active = .{ .x = 0, .y = 0 } });
|
||||
const page_rac = page_offset.rowAndCell(0);
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.cols = cols,
|
||||
.rows = rows,
|
||||
.page_pool = pool,
|
||||
.pages = page_list,
|
||||
.viewport = page,
|
||||
.viewport_row = 0,
|
||||
.pages = pages,
|
||||
.cursor = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.page = page,
|
||||
.page_row = 0,
|
||||
.page_row_ptr = cursor_row_ptr,
|
||||
.page_cell_ptr = cursor_cell_ptr,
|
||||
.page_offset = page_offset,
|
||||
.page_row = page_rac.row,
|
||||
.page_cell = page_rac.cell,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Screen) void {
|
||||
// Deallocate all the pages. We don't need to deallocate the list or
|
||||
// nodes because they all reside in the pool.
|
||||
while (self.pages.popFirst()) |node| node.data.deinit(self.alloc);
|
||||
self.page_pool.deinit();
|
||||
self.pages.deinit();
|
||||
}
|
||||
|
||||
/// Dump the screen to a string. The writer given should be buffered;
|
||||
/// this function does not attempt to efficiently write and generally writes
|
||||
/// one byte at a time.
|
||||
pub fn dumpString(
|
||||
self: *const Screen,
|
||||
writer: anytype,
|
||||
tl: point.Point,
|
||||
) !void {
|
||||
var blank_rows: usize = 0;
|
||||
|
||||
var iter = self.pages.rowIterator(tl);
|
||||
while (iter.next()) |row_offset| {
|
||||
const rac = row_offset.rowAndCell(0);
|
||||
const cells = cells: {
|
||||
const cells: [*]pagepkg.Cell = @ptrCast(rac.cell);
|
||||
break :cells cells[0..self.pages.cols];
|
||||
};
|
||||
|
||||
if (blank_rows > 0) {
|
||||
for (0..blank_rows) |_| try writer.writeByte('\n');
|
||||
blank_rows = 0;
|
||||
}
|
||||
|
||||
// TODO: handle wrap
|
||||
blank_rows += 1;
|
||||
|
||||
for (cells) |cell| {
|
||||
// TODO: handle blanks between chars
|
||||
if (cell.codepoint == 0) break;
|
||||
try writer.print("{u}", .{cell.codepoint});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dumpStringAlloc(
|
||||
self: *const Screen,
|
||||
alloc: Allocator,
|
||||
tl: point.Point,
|
||||
) ![]const u8 {
|
||||
var builder = std.ArrayList(u8).init(alloc);
|
||||
defer builder.deinit();
|
||||
try self.dumpString(builder.writer(), tl);
|
||||
return try builder.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||
const view = try std.unicode.Utf8View.init(text);
|
||||
var iter = view.iterator();
|
||||
while (iter.nextCodepoint()) |c| {
|
||||
if (self.cursor.x == self.cols) {
|
||||
if (self.cursor.x == self.pages.cols) {
|
||||
@panic("wrap not implemented");
|
||||
}
|
||||
|
||||
@ -151,11 +130,11 @@ fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||
assert(width == 1 or width == 2);
|
||||
switch (width) {
|
||||
1 => {
|
||||
self.cursor.page_cell_ptr.codepoint = c;
|
||||
self.cursor.page_cell.codepoint = c;
|
||||
self.cursor.x += 1;
|
||||
if (self.cursor.x < self.cols) {
|
||||
const cell_ptr: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell_ptr);
|
||||
self.cursor.page_cell_ptr = @ptrCast(cell_ptr + 1);
|
||||
if (self.cursor.x < self.pages.cols) {
|
||||
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
|
||||
self.cursor.page_cell = @ptrCast(cell + 1);
|
||||
} else {
|
||||
@panic("wrap not implemented");
|
||||
}
|
||||
@ -175,4 +154,7 @@ test "Screen read and write" {
|
||||
defer s.deinit();
|
||||
|
||||
try s.testWriteString("hello, world");
|
||||
const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
|
||||
defer alloc.free(str);
|
||||
//try testing.expectEqualStrings("hello, world", str);
|
||||
}
|
||||
|
@ -107,6 +107,25 @@ pub const Page = struct {
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Get a single row. y must be valid.
|
||||
pub fn getRow(self: *const Page, y: usize) *Row {
|
||||
assert(y < self.capacity.rows);
|
||||
return &self.rows.ptr(self.memory)[y];
|
||||
}
|
||||
|
||||
/// Get the cells for a row.
|
||||
pub fn getCells(self: *const Page, row: *Row) []Cell {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
const rows = self.rows.ptr(self.memory);
|
||||
const cells = self.cells.ptr(self.memory);
|
||||
assert(@intFromPtr(row) >= @intFromPtr(rows));
|
||||
assert(@intFromPtr(row) < @intFromPtr(cells));
|
||||
}
|
||||
|
||||
const cells = row.cells.ptr(self.memory);
|
||||
return cells[0..self.capacity.cols];
|
||||
}
|
||||
|
||||
/// Get the row and cell for the given X/Y within this page.
|
||||
pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct {
|
||||
row: *Row,
|
||||
|
71
src/terminal/new/point.zig
Normal file
71
src/terminal/new/point.zig
Normal file
@ -0,0 +1,71 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
/// The possible reference locations for a point. When someone says
|
||||
/// "(42, 80)" in the context of a terminal, that could mean multiple
|
||||
/// things: it is in the current visible viewport? the current active
|
||||
/// area of the screen where the cursor is? the entire scrollback history?
|
||||
/// etc. This tag is used to differentiate those cases.
|
||||
pub const Tag = enum {
|
||||
/// Top-left is part of the active area where a running program can
|
||||
/// jump the cursor and make changes. The active area is the "editable"
|
||||
/// part of the screen.
|
||||
///
|
||||
/// The bottom-right of the active tag differs from all other tags
|
||||
/// because it includes the full height (rows) of the screen, including
|
||||
/// rows that may not be written yet. This is required because the active
|
||||
/// area is fully "addressable" by the running program (see below) whereas
|
||||
/// the other tags are used primarliy for reading/modifying past-written
|
||||
/// data so they can't address unwritten rows.
|
||||
///
|
||||
/// Note for those less familiar with terminal functionality: there
|
||||
/// are escape sequences to move the cursor to any position on
|
||||
/// the screen, but it is limited to the size of the viewport and
|
||||
/// the bottommost part of the screen. Terminal programs can't --
|
||||
/// with sequences at the time of writing this comment -- modify
|
||||
/// anything in the scrollback, visible viewport (if it differs
|
||||
/// from the active area), etc.
|
||||
active,
|
||||
|
||||
/// Top-left is the visible viewport. This means that if the user
|
||||
/// has scrolled in any direction, top-left changes. The bottom-right
|
||||
/// is the last written row from the top-left.
|
||||
viewport,
|
||||
|
||||
/// Top-left is the furthest back in the scrollback history
|
||||
/// supported by the screen and the bottom-right is the bottom-right
|
||||
/// of the last written row. Note this last point is important: the
|
||||
/// bottom right is NOT necessarilly the same as "active" because
|
||||
/// "active" always allows referencing the full rows tall of the
|
||||
/// screen whereas "screen" only contains written rows.
|
||||
screen,
|
||||
|
||||
/// The top-left is the same as "screen" but the bottom-right is
|
||||
/// the line just before the top of "active". This contains only
|
||||
/// the scrollback history.
|
||||
history,
|
||||
};
|
||||
|
||||
/// An x/y point in the terminal for some definition of location (tag).
|
||||
pub const Point = union(Tag) {
|
||||
active: Coordinate,
|
||||
viewport: Coordinate,
|
||||
screen: Coordinate,
|
||||
history: Coordinate,
|
||||
|
||||
pub const Coordinate = struct {
|
||||
x: usize = 0,
|
||||
y: usize = 0,
|
||||
};
|
||||
|
||||
pub fn coord(self: Point) Coordinate {
|
||||
return switch (self) {
|
||||
.active,
|
||||
.viewport,
|
||||
.screen,
|
||||
.history,
|
||||
=> |v| v,
|
||||
};
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user