mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 02:36:22 +03:00
411 lines
12 KiB
Zig
411 lines
12 KiB
Zig
const Screen = @This();
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const color = @import("color.zig");
|
|
|
|
const log = std.log.scoped(.screen);
|
|
|
|
/// A row is a set of cells.
|
|
pub const Row = []Cell;
|
|
|
|
/// Cell is a single cell within the screen.
|
|
pub const Cell = struct {
|
|
/// Each cell contains exactly one character. The character is UTF-32
|
|
/// encoded (just the Unicode codepoint).
|
|
char: u32,
|
|
|
|
/// Foreground and background color. null means to use the default.
|
|
fg: ?color.RGB = null,
|
|
bg: ?color.RGB = null,
|
|
|
|
/// True if the cell should be skipped for drawing
|
|
pub fn empty(self: Cell) bool {
|
|
return self.char == 0;
|
|
}
|
|
};
|
|
|
|
pub const RowIterator = struct {
|
|
screen: *const Screen,
|
|
index: usize,
|
|
|
|
pub fn next(self: *RowIterator) ?Row {
|
|
if (self.index >= self.screen.rows) return null;
|
|
const res = self.screen.getRow(self.index);
|
|
self.index += 1;
|
|
return res;
|
|
}
|
|
};
|
|
|
|
/// The full list of rows, including any scrollback.
|
|
storage: []Cell,
|
|
|
|
/// The first visible row.
|
|
zero: usize,
|
|
|
|
/// The number of rows and columns in the visible space.
|
|
rows: usize,
|
|
cols: usize,
|
|
|
|
/// Initialize a new screen.
|
|
pub fn init(alloc: Allocator, rows: usize, cols: usize) !Screen {
|
|
// Allocate enough storage to cover every row and column in the visible
|
|
// area. This wastes some up front memory but saves allocations later.
|
|
const buf = try alloc.alloc(Cell, rows * cols);
|
|
std.mem.set(Cell, buf, .{ .char = 0 });
|
|
|
|
return Screen{
|
|
.storage = buf,
|
|
.zero = 0,
|
|
.rows = rows,
|
|
.cols = cols,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Screen, alloc: Allocator) void {
|
|
alloc.free(self.storage);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Returns an iterator that can be used to iterate over all of the rows
|
|
/// from index zero.
|
|
pub fn rowIterator(self: *const Screen) RowIterator {
|
|
return .{ .screen = self, .index = 0 };
|
|
}
|
|
|
|
/// Get the visible portion of the screen.
|
|
pub fn getVisible(self: Screen) []Cell {
|
|
return self.storage;
|
|
}
|
|
|
|
/// Get a single row by index (0-indexed).
|
|
pub fn getRow(self: Screen, idx: usize) Row {
|
|
// Get the index of the first byte of the the row at index.
|
|
const real_idx = self.rowIndex(idx);
|
|
|
|
// The storage is sliced to return exactly the number of columns.
|
|
return self.storage[real_idx .. real_idx + self.cols];
|
|
}
|
|
|
|
/// Get a single cell in the visible area. row and col are 0-indexed.
|
|
pub fn getCell(self: Screen, row: usize, col: usize) *Cell {
|
|
assert(row < self.rows);
|
|
assert(col < self.cols);
|
|
const row_idx = self.rowIndex(row);
|
|
return &self.storage[row_idx + col];
|
|
}
|
|
|
|
/// Returns the index for the given row (0-indexed) into the underlying
|
|
/// storage array.
|
|
pub fn rowIndex(self: Screen, idx: usize) usize {
|
|
assert(idx < self.rows);
|
|
const val = (self.zero + idx) * self.cols;
|
|
if (val < self.storage.len) return val;
|
|
return val - self.storage.len;
|
|
}
|
|
|
|
/// Scroll the screen up (positive) or down (negative). Scrolling direction
|
|
/// is the direction text would move. For example, scrolling down would
|
|
/// move existing text downward.
|
|
pub fn scroll(self: *Screen, count: isize) void {
|
|
if (count < 0) {
|
|
const amount = @mod(@intCast(usize, -count), self.rows);
|
|
if (amount > self.zero) {
|
|
self.zero = self.rows - amount;
|
|
} else {
|
|
self.zero -|= amount;
|
|
}
|
|
} else {
|
|
self.zero = @mod(self.zero + @intCast(usize, count), self.rows);
|
|
}
|
|
}
|
|
|
|
/// Copy row at src to dst.
|
|
pub fn copyRow(self: *Screen, dst: usize, src: usize) void {
|
|
const src_row = self.getRow(src);
|
|
const dst_row = self.getRow(dst);
|
|
std.mem.copy(Cell, dst_row, src_row);
|
|
}
|
|
|
|
/// Resize the screen. The rows or cols can be bigger or smaller. Due to
|
|
/// the internal representation of a screen, this usually involves a significant
|
|
/// amount of copying compared to any other operations.
|
|
///
|
|
/// This will trim data if the size is getting smaller. It is expected that
|
|
/// callers will reflow the text prior to calling this.
|
|
pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void {
|
|
// Make a copy so we can access the old indexes.
|
|
const old = self.*;
|
|
|
|
// Reallocate the storage
|
|
self.storage = try alloc.alloc(Cell, rows * cols);
|
|
self.zero = 0;
|
|
self.rows = rows;
|
|
self.cols = cols;
|
|
|
|
// If we're increasing height, then copy all rows (start at 0).
|
|
// Otherwise start at the latest row that includes the bottom row,
|
|
// aka trip the top.
|
|
var y: usize = if (rows >= old.rows) 0 else old.rows - rows;
|
|
const start = y;
|
|
const col_end = @minimum(old.cols, cols);
|
|
while (y < old.rows) : (y += 1) {
|
|
// Copy the old row into the new row, just losing the columsn
|
|
// if we got thinner.
|
|
const old_row = old.getRow(y);
|
|
const new_row = self.getRow(y - start);
|
|
std.mem.copy(Cell, new_row, old_row[0..col_end]);
|
|
|
|
// If our new row is wider, then we copy zeroes into the rest.
|
|
if (new_row.len > old_row.len) {
|
|
std.mem.set(Cell, new_row[old_row.len..], .{ .char = 0 });
|
|
}
|
|
}
|
|
|
|
// If we grew rows, then set the remaining data to zero.
|
|
if (rows > old.rows) {
|
|
std.mem.set(Cell, self.storage[self.rowIndex(old.rows)..], .{ .char = 0 });
|
|
}
|
|
|
|
// Free the old data
|
|
alloc.free(old.storage);
|
|
}
|
|
|
|
/// Turns the screen into a string.
|
|
pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
|
const buf = try alloc.alloc(u8, self.storage.len + self.rows);
|
|
|
|
var i: usize = 0;
|
|
var y: usize = 0;
|
|
var rows = self.rowIterator();
|
|
while (rows.next()) |row| {
|
|
defer y += 1;
|
|
|
|
if (y > 0) {
|
|
buf[i] = '\n';
|
|
i += 1;
|
|
}
|
|
|
|
for (row) |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);
|
|
}
|
|
|
|
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
|
/// each row.
|
|
fn testWriteString(self: *Screen, text: []const u8) void {
|
|
var y: usize = 0;
|
|
var x: usize = 0;
|
|
var row = self.getRow(y);
|
|
for (text) |c| {
|
|
if (c == '\n') {
|
|
y += 1;
|
|
x = 0;
|
|
row = self.getRow(y);
|
|
continue;
|
|
}
|
|
|
|
assert(x < self.cols);
|
|
row[x].char = @intCast(u32, c);
|
|
x += 1;
|
|
}
|
|
}
|
|
|
|
test "Screen" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
|
|
// Sanity check that our test helpers work
|
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
|
s.testWriteString(str);
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings(str, contents);
|
|
|
|
// Test the row iterator
|
|
var count: usize = 0;
|
|
var iter = s.rowIterator();
|
|
while (iter.next()) |row| {
|
|
// Rows should be pointer equivalent to getRow
|
|
const row_other = s.getRow(count);
|
|
try testing.expectEqual(row.ptr, row_other.ptr);
|
|
count += 1;
|
|
}
|
|
|
|
// Should go through all rows
|
|
try testing.expectEqual(@as(usize, 3), count);
|
|
}
|
|
|
|
test "Screen: scrolling" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
|
s.scroll(1);
|
|
|
|
// Test our row index
|
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
|
|
try testing.expectEqual(@as(usize, 0), s.rowIndex(2));
|
|
|
|
{
|
|
// Test our contents rotated
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings("2EFGH\n3IJKL\n1ABCD", contents);
|
|
}
|
|
|
|
// Scroll by a multiple
|
|
s.scroll(@intCast(isize, s.rows) * 4);
|
|
|
|
// Test our row index
|
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
|
|
try testing.expectEqual(@as(usize, 0), s.rowIndex(2));
|
|
|
|
{
|
|
// Test our contents rotated
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings("2EFGH\n3IJKL\n1ABCD", contents);
|
|
}
|
|
}
|
|
|
|
test "Screen: scroll down from 0" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
|
s.scroll(-1);
|
|
|
|
// Test our row index
|
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(0));
|
|
try testing.expectEqual(@as(usize, 0), s.rowIndex(1));
|
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(2));
|
|
|
|
{
|
|
// Test our contents rotated
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings("3IJKL\n1ABCD\n2EFGH", contents);
|
|
}
|
|
|
|
// Scroll by a multiple
|
|
s.scroll(-4 * @intCast(isize, s.rows));
|
|
|
|
// Test our row index
|
|
try testing.expectEqual(@as(usize, 10), s.rowIndex(0));
|
|
try testing.expectEqual(@as(usize, 0), s.rowIndex(1));
|
|
try testing.expectEqual(@as(usize, 5), s.rowIndex(2));
|
|
|
|
{
|
|
// Test our contents rotated
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings("3IJKL\n1ABCD\n2EFGH", contents);
|
|
}
|
|
}
|
|
|
|
test "Screen: row copy" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
|
|
|
// Copy
|
|
s.scroll(1);
|
|
s.copyRow(2, 0);
|
|
|
|
// Test our contents
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents);
|
|
}
|
|
|
|
test "Screen: resize more rows" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
|
s.testWriteString(str);
|
|
try s.resize(alloc, 10, 5);
|
|
|
|
{
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings(str, contents);
|
|
}
|
|
}
|
|
|
|
test "Screen: resize less rows" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
|
s.testWriteString(str);
|
|
try s.resize(alloc, 2, 5);
|
|
|
|
{
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
|
|
}
|
|
}
|
|
|
|
test "Screen: resize more cols" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
|
s.testWriteString(str);
|
|
try s.resize(alloc, 3, 10);
|
|
|
|
{
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
try testing.expectEqualStrings(str, contents);
|
|
}
|
|
}
|
|
|
|
test "Screen: resize less cols" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var s = try init(alloc, 3, 5);
|
|
defer s.deinit(alloc);
|
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
|
s.testWriteString(str);
|
|
try s.resize(alloc, 3, 4);
|
|
|
|
{
|
|
var contents = try s.testString(alloc);
|
|
defer alloc.free(contents);
|
|
const expected = "1ABC\n2EFG\n3IJK";
|
|
try testing.expectEqualStrings(expected, contents);
|
|
}
|
|
}
|