mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
big API surface for screen2, can test write/read now
This commit is contained in:
@ -20,6 +20,7 @@ const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const utf8proc = @import("utf8proc");
|
||||
const color = @import("color.zig");
|
||||
const CircBuf = @import("circ_buf.zig").CircBuf;
|
||||
|
||||
@ -31,7 +32,7 @@ const log = std.log.scoped(.screen);
|
||||
/// Note: the union is extern so that it follows the same memory layout
|
||||
/// semantics as C, which allows us to have a tightly packed union.
|
||||
const StorageCell = extern union {
|
||||
row_header: RowHeader,
|
||||
header: RowHeader,
|
||||
cell: Cell,
|
||||
|
||||
test {
|
||||
@ -62,7 +63,7 @@ const RowHeader = struct {
|
||||
};
|
||||
|
||||
/// Cell is a single cell within the screen.
|
||||
const Cell = struct {
|
||||
pub const Cell = struct {
|
||||
/// The primary unicode codepoint for this cell. Most cells (almost all)
|
||||
/// contain exactly one unicode codepoint. However, it is possible for
|
||||
/// cells to contain multiple if multiple codepoints are used to create
|
||||
@ -117,6 +118,158 @@ const Cell = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// A row is a single row in the screen.
|
||||
pub const Row = struct {
|
||||
/// Raw internal storage, do NOT write to this, use only the
|
||||
/// helpers. Writing directly to this can easily mess up state
|
||||
/// causing future crashes or misrendering.
|
||||
storage: []StorageCell,
|
||||
|
||||
/// Set that this row is soft-wrapped. This doesn't change the contents
|
||||
/// of this row so the row won't be marked dirty.
|
||||
pub fn setWrapped(self: Row, v: bool) void {
|
||||
self.storage[0].header.wrap = v;
|
||||
}
|
||||
|
||||
/// Get a pointr to the cell at column x (0-indexed). This always
|
||||
/// assumes that the cell was modified, notifying the renderer on the
|
||||
/// next call to re-render this cell. Any change detection to avoid
|
||||
/// this should be done prior.
|
||||
pub fn getCellPtr(self: Row, x: usize) *Cell {
|
||||
assert(x < self.storage.len - 1);
|
||||
return &self.storage[x + 1].cell;
|
||||
}
|
||||
|
||||
/// Read-only iterator for the cells in the row.
|
||||
pub fn cellIterator(self: Row) CellIterator {
|
||||
return .{ .row = self };
|
||||
}
|
||||
};
|
||||
|
||||
/// Used to iterate through the rows of a specific region.
|
||||
pub const RowIterator = struct {
|
||||
screen: *Screen,
|
||||
tag: RowIndexTag,
|
||||
value: usize = 0,
|
||||
|
||||
pub fn next(self: *RowIterator) ?Row {
|
||||
if (self.value >= self.tag.maxLen(self.screen)) return null;
|
||||
const idx = self.tag.index(self.value);
|
||||
const res = self.screen.getRow(idx);
|
||||
self.value += 1;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
/// Used to iterate through the rows of a specific region.
|
||||
pub const CellIterator = struct {
|
||||
row: Row,
|
||||
i: usize = 0,
|
||||
|
||||
pub fn next(self: *CellIterator) ?Cell {
|
||||
if (self.i >= self.row.storage.len - 1) return null;
|
||||
const res = self.row.storage[self.i + 1].cell;
|
||||
self.i += 1;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
/// RowIndex represents a row within the screen. There are various meanings
|
||||
/// of a row index and this union represents the available types. For example,
|
||||
/// when talking about row "0" you may want the first row in the viewport,
|
||||
/// the first row in the scrollback, or the first row in the active area.
|
||||
///
|
||||
/// All row indexes are 0-indexed.
|
||||
pub const RowIndex = union(RowIndexTag) {
|
||||
/// The index is from the top of the screen. The screen includes all
|
||||
/// the history.
|
||||
screen: usize,
|
||||
|
||||
/// The index is from the top of the viewport. Therefore, depending
|
||||
/// on where the user has scrolled the viewport, "0" is different.
|
||||
viewport: usize,
|
||||
|
||||
/// The index is from the top of the active area. The active area is
|
||||
/// always "rows" tall, and 0 is the top row. The active area is the
|
||||
/// "edit-able" area where the terminal cursor is.
|
||||
active: usize,
|
||||
|
||||
/// The index is from the top of the history (scrollback) to just
|
||||
/// prior to the active area.
|
||||
history: usize,
|
||||
|
||||
/// Convert this row index into a screen offset. This will validate
|
||||
/// the value so even if it is already a screen value, this may error.
|
||||
pub fn toScreen(self: RowIndex, screen: *const Screen) RowIndex {
|
||||
const y = switch (self) {
|
||||
.screen => |y| y: {
|
||||
assert(y < RowIndexTag.screen.maxLen(screen));
|
||||
break :y y;
|
||||
},
|
||||
|
||||
.viewport => |y| y: {
|
||||
assert(y < RowIndexTag.viewport.maxLen(screen));
|
||||
break :y y + screen.viewport;
|
||||
},
|
||||
|
||||
.active => |y| y: {
|
||||
assert(y < RowIndexTag.active.maxLen(screen));
|
||||
break :y RowIndexTag.history.maxLen(screen) + y;
|
||||
},
|
||||
|
||||
.history => |y| y: {
|
||||
assert(y < RowIndexTag.history.maxLen(screen));
|
||||
break :y y;
|
||||
},
|
||||
};
|
||||
|
||||
return .{ .screen = y };
|
||||
}
|
||||
};
|
||||
|
||||
/// The tags of RowIndex
|
||||
pub const RowIndexTag = enum {
|
||||
screen,
|
||||
viewport,
|
||||
active,
|
||||
history,
|
||||
|
||||
/// The max length for a given tag. This is a length, not an index,
|
||||
/// so it is 1-indexed. If the value is zero, it means that this
|
||||
/// section of the screen is empty or disabled.
|
||||
pub fn maxLen(self: RowIndexTag, screen: *const Screen) usize {
|
||||
const rows_written = screen.rowsWritten();
|
||||
|
||||
return switch (self) {
|
||||
// Screen can be any of the written rows
|
||||
.screen => rows_written,
|
||||
|
||||
// Viewport can be any of the written rows or the max size
|
||||
// of a viewport.
|
||||
.viewport => @minimum(screen.rows, rows_written),
|
||||
|
||||
// History is all the way up to the top of our active area. If
|
||||
// we haven't filled our active area, there is no history.
|
||||
.history => if (rows_written > screen.rows) rows_written - screen.rows else 0,
|
||||
|
||||
// Active area can be any number of rows. We ignore rows
|
||||
// written here because this is the only row index that can
|
||||
// actively grow our rows.
|
||||
.active => screen.rows,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a RowIndex from a tag.
|
||||
pub fn index(self: RowIndexTag, value: usize) RowIndex {
|
||||
return switch (self) {
|
||||
.screen => .{ .screen = value },
|
||||
.viewport => .{ .viewport = value },
|
||||
.active => .{ .active = value },
|
||||
.history => .{ .history = value },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const StorageBuf = CircBuf(StorageCell);
|
||||
|
||||
/// The allocator used for all the storage operations
|
||||
@ -133,6 +286,9 @@ cols: usize,
|
||||
/// is in addition to the number of visible rows.
|
||||
max_scrollback: usize,
|
||||
|
||||
/// The row (offset from the top) where the viewport currently is.
|
||||
viewport: usize,
|
||||
|
||||
/// Initialize a new screen.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
@ -151,6 +307,7 @@ pub fn init(
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
.max_scrollback = max_scrollback,
|
||||
.viewport = 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -158,10 +315,176 @@ pub fn deinit(self: *Screen) void {
|
||||
self.storage.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// Returns an iterator that can be used to iterate over all of the rows
|
||||
/// from index zero of the given row index type. This can therefore iterate
|
||||
/// from row 0 of the active area, history, viewport, etc.
|
||||
pub fn rowIterator(self: *Screen, tag: RowIndexTag) RowIterator {
|
||||
return .{ .screen = self, .tag = tag };
|
||||
}
|
||||
|
||||
/// Returns the row at the given index. This row is writable, although
|
||||
/// only the active area should probably be written to.
|
||||
pub fn getRow(self: *Screen, index: RowIndex) Row {
|
||||
// Get our offset into storage
|
||||
const offset = index.toScreen(self).screen * (self.cols + 1);
|
||||
|
||||
// Get the slices into the storage. This should never wrap because
|
||||
// we're perfectly aligned on row boundaries.
|
||||
const slices = self.storage.getPtrSlice(offset, self.cols + 1);
|
||||
assert(slices[0].len == self.cols + 1 and slices[1].len == 0);
|
||||
|
||||
return .{ .storage = slices[0] };
|
||||
}
|
||||
|
||||
/// Returns the offset into the storage buffer that the given row can
|
||||
/// be found. This assumes valid input and will crash if the input is
|
||||
/// invalid.
|
||||
fn rowOffset(self: Screen, index: RowIndex) usize {
|
||||
// +1 for row header
|
||||
return index.toScreen().screen * (self.cols + 1);
|
||||
}
|
||||
|
||||
fn rowsWritten(self: Screen) usize {
|
||||
// The number of rows we've actually written into our buffer
|
||||
// This should always be cleanly divisible since we only request
|
||||
// data in row chunks from the buffer.
|
||||
assert(@mod(self.storage.len(), self.cols + 1) == 0);
|
||||
return self.storage.len() / (self.cols + 1);
|
||||
}
|
||||
|
||||
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
||||
/// each row. If a line is longer than the available columns, soft-wrapping
|
||||
/// will occur. This will automatically handle basic wide chars.
|
||||
pub fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
var y: usize = 0;
|
||||
var x: usize = 0;
|
||||
|
||||
const view = std.unicode.Utf8View.init(text) catch unreachable;
|
||||
var iter = view.iterator();
|
||||
while (iter.nextCodepoint()) |c| {
|
||||
// Explicit newline forces a new row
|
||||
if (c == '\n') {
|
||||
y += 1;
|
||||
x = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're writing past the end of the active area, scroll.
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
@panic("TODO");
|
||||
//self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
|
||||
// Get our row
|
||||
var row = self.getRow(.{ .active = y });
|
||||
|
||||
// If we're writing past the end, we need to soft wrap.
|
||||
if (x == self.cols) {
|
||||
row.setWrapped(true);
|
||||
y += 1;
|
||||
x = 0;
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
@panic("TODO");
|
||||
//self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
|
||||
// If our character is double-width, handle it.
|
||||
const width = utf8proc.charwidth(c);
|
||||
assert(width == 1 or width == 2);
|
||||
switch (width) {
|
||||
1 => {
|
||||
const cell = row.getCellPtr(x);
|
||||
cell.char = @intCast(u32, c);
|
||||
},
|
||||
|
||||
2 => {
|
||||
if (x == self.cols - 1) {
|
||||
const cell = row.getCellPtr(x);
|
||||
cell.char = ' ';
|
||||
cell.attrs.wide_spacer_head = true;
|
||||
|
||||
// wrap
|
||||
row.setWrapped(true);
|
||||
y += 1;
|
||||
x = 0;
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
@panic("TODO");
|
||||
//self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
|
||||
{
|
||||
const cell = row.getCellPtr(x);
|
||||
cell.char = @intCast(u32, c);
|
||||
cell.attrs.wide = true;
|
||||
}
|
||||
|
||||
{
|
||||
x += 1;
|
||||
const cell = row.getCellPtr(x);
|
||||
cell.char = ' ';
|
||||
cell.attrs.wide_spacer_tail = true;
|
||||
}
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns the screen into a string. Different regions of the screen can
|
||||
/// be selected using the "tag", i.e. if you want to output the viewport,
|
||||
/// the scrollback, the full screen, etc.
|
||||
///
|
||||
/// This is only useful for testing.
|
||||
pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 {
|
||||
const buf = try alloc.alloc(u8, self.storage.len() * 4);
|
||||
|
||||
var i: usize = 0;
|
||||
var y: usize = 0;
|
||||
var rows = self.rowIterator(tag);
|
||||
while (rows.next()) |row| {
|
||||
defer y += 1;
|
||||
|
||||
if (y > 0) {
|
||||
buf[i] = '\n';
|
||||
i += 1;
|
||||
}
|
||||
|
||||
var cells = row.cellIterator();
|
||||
while (cells.next()) |cell| {
|
||||
// TODO: handle character after null
|
||||
if (cell.char > 0) {
|
||||
i += try std.unicode.utf8Encode(@intCast(u21, cell.char), buf[i..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Never render the final newline
|
||||
const str = std.mem.trimRight(u8, buf[0..i], "\n");
|
||||
return try alloc.realloc(buf, str.len);
|
||||
}
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||
s.testWriteString(str);
|
||||
{
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings(str, contents);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user