mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 17:26: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/page.zig");
|
||||||
_ = @import("new/PageList.zig");
|
_ = @import("new/PageList.zig");
|
||||||
_ = @import("new/Screen.zig");
|
_ = @import("new/Screen.zig");
|
||||||
|
_ = @import("new/point.zig");
|
||||||
_ = @import("new/size.zig");
|
_ = @import("new/size.zig");
|
||||||
_ = @import("new/style.zig");
|
_ = @import("new/style.zig");
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ const PageList = @This();
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
const point = @import("point.zig");
|
||||||
const pagepkg = @import("page.zig");
|
const pagepkg = @import("page.zig");
|
||||||
const Page = pagepkg.Page;
|
const Page = pagepkg.Page;
|
||||||
|
|
||||||
@ -41,10 +42,15 @@ pool: Pool,
|
|||||||
/// The list of pages in the screen.
|
/// The list of pages in the screen.
|
||||||
pages: List,
|
pages: List,
|
||||||
|
|
||||||
/// The page that contains the top of the current viewport and the row
|
/// The top-left of certain parts of the screen that are frequently
|
||||||
/// within that page that is the top of the viewport (0-indexed).
|
/// accessed so we don't have to traverse the linked list to find them.
|
||||||
viewport: *List.Node,
|
///
|
||||||
viewport_row: usize,
|
/// 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
|
/// The current desired screen dimensions. I say "desired" because individual
|
||||||
/// pages may still be a different size and not yet reflowed since we lazily
|
/// pages may still be a different size and not yet reflowed since we lazily
|
||||||
@ -87,8 +93,8 @@ pub fn init(
|
|||||||
.rows = rows,
|
.rows = rows,
|
||||||
.pool = pool,
|
.pool = pool,
|
||||||
.pages = page_list,
|
.pages = page_list,
|
||||||
.viewport = page,
|
.viewport = .{ .page = page },
|
||||||
.viewport_row = 0,
|
.active = .{ .page = page },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +105,125 @@ pub fn deinit(self: *PageList) void {
|
|||||||
self.pool.deinit();
|
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" {
|
test "PageList" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -4,58 +4,21 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const unicode = @import("../../unicode/main.zig");
|
const unicode = @import("../../unicode/main.zig");
|
||||||
|
const PageList = @import("PageList.zig");
|
||||||
const pagepkg = @import("page.zig");
|
const pagepkg = @import("page.zig");
|
||||||
|
const point = @import("point.zig");
|
||||||
const Page = pagepkg.Page;
|
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.
|
/// The general purpose allocator to use for all memory allocations.
|
||||||
/// Unfortunately some screen operations do require allocation.
|
/// Unfortunately some screen operations do require allocation.
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
/// The memory pool we get page nodes for the linked list from.
|
|
||||||
page_pool: PagePool,
|
|
||||||
|
|
||||||
/// The list of pages in the screen.
|
/// The list of pages in the screen.
|
||||||
pages: PageList,
|
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
|
/// The current cursor position
|
||||||
cursor: Cursor,
|
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.
|
/// The cursor position.
|
||||||
const Cursor = struct {
|
const Cursor = struct {
|
||||||
// The x/y position within the viewport.
|
// The x/y position within the viewport.
|
||||||
@ -66,12 +29,11 @@ const Cursor = struct {
|
|||||||
/// next character print will force a soft-wrap.
|
/// next character print will force a soft-wrap.
|
||||||
pending_wrap: bool = false,
|
pending_wrap: bool = false,
|
||||||
|
|
||||||
// The page that the cursor is on and the offset into that page that
|
/// The pointers into the page list where the cursor is currently
|
||||||
// the current y exists.
|
/// located. This makes it faster to move the cursor.
|
||||||
page: *PageList.Node,
|
page_offset: PageList.RowOffset,
|
||||||
page_row: usize,
|
page_row: *pagepkg.Row,
|
||||||
page_row_ptr: *pagepkg.Row,
|
page_cell: *pagepkg.Cell,
|
||||||
page_cell_ptr: *pagepkg.Cell,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Initialize a new screen.
|
/// Initialize a new screen.
|
||||||
@ -81,65 +43,82 @@ pub fn init(
|
|||||||
rows: usize,
|
rows: usize,
|
||||||
max_scrollback: usize,
|
max_scrollback: usize,
|
||||||
) !Screen {
|
) !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,
|
// The viewport is guaranteed to exist, so grab it so we can setup
|
||||||
// and we'll split it thereafter if it gets too large and add more as
|
// our initial cursor.
|
||||||
// necessary.
|
const page_offset = pages.rowOffset(.{ .active = .{ .x = 0, .y = 0 } });
|
||||||
var pool = try PagePool.initPreheated(alloc, page_preheat);
|
const page_rac = page_offset.rowAndCell(0);
|
||||||
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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.cols = cols,
|
.pages = pages,
|
||||||
.rows = rows,
|
|
||||||
.page_pool = pool,
|
|
||||||
.pages = page_list,
|
|
||||||
.viewport = page,
|
|
||||||
.viewport_row = 0,
|
|
||||||
.cursor = .{
|
.cursor = .{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = 0,
|
.y = 0,
|
||||||
.page = page,
|
.page_offset = page_offset,
|
||||||
.page_row = 0,
|
.page_row = page_rac.row,
|
||||||
.page_row_ptr = cursor_row_ptr,
|
.page_cell = page_rac.cell,
|
||||||
.page_cell_ptr = cursor_cell_ptr,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Screen) void {
|
pub fn deinit(self: *Screen) void {
|
||||||
// Deallocate all the pages. We don't need to deallocate the list or
|
self.pages.deinit();
|
||||||
// nodes because they all reside in the pool.
|
}
|
||||||
while (self.pages.popFirst()) |node| node.data.deinit(self.alloc);
|
|
||||||
self.page_pool.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 {
|
fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||||
const view = try std.unicode.Utf8View.init(text);
|
const view = try std.unicode.Utf8View.init(text);
|
||||||
var iter = view.iterator();
|
var iter = view.iterator();
|
||||||
while (iter.nextCodepoint()) |c| {
|
while (iter.nextCodepoint()) |c| {
|
||||||
if (self.cursor.x == self.cols) {
|
if (self.cursor.x == self.pages.cols) {
|
||||||
@panic("wrap not implemented");
|
@panic("wrap not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,11 +130,11 @@ fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
assert(width == 1 or width == 2);
|
assert(width == 1 or width == 2);
|
||||||
switch (width) {
|
switch (width) {
|
||||||
1 => {
|
1 => {
|
||||||
self.cursor.page_cell_ptr.codepoint = c;
|
self.cursor.page_cell.codepoint = c;
|
||||||
self.cursor.x += 1;
|
self.cursor.x += 1;
|
||||||
if (self.cursor.x < self.cols) {
|
if (self.cursor.x < self.pages.cols) {
|
||||||
const cell_ptr: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell_ptr);
|
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
|
||||||
self.cursor.page_cell_ptr = @ptrCast(cell_ptr + 1);
|
self.cursor.page_cell = @ptrCast(cell + 1);
|
||||||
} else {
|
} else {
|
||||||
@panic("wrap not implemented");
|
@panic("wrap not implemented");
|
||||||
}
|
}
|
||||||
@ -175,4 +154,7 @@ test "Screen read and write" {
|
|||||||
defer s.deinit();
|
defer s.deinit();
|
||||||
|
|
||||||
try s.testWriteString("hello, world");
|
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;
|
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.
|
/// Get the row and cell for the given X/Y within this page.
|
||||||
pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct {
|
pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct {
|
||||||
row: *Row,
|
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