ghostty/src/terminal/page.zig

1281 lines
44 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const testing = std.testing;
const fastmem = @import("../fastmem.zig");
const color = @import("color.zig");
const sgr = @import("sgr.zig");
const style = @import("style.zig");
const size = @import("size.zig");
const getOffset = size.getOffset;
const Offset = size.Offset;
const OffsetBuf = size.OffsetBuf;
const BitmapAllocator = @import("bitmap_allocator.zig").BitmapAllocator;
const hash_map = @import("hash_map.zig");
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
const alignForward = std.mem.alignForward;
const alignBackward = std.mem.alignBackward;
/// The allocator to use for multi-codepoint grapheme data. We use
/// a chunk size of 4 codepoints. It'd be best to set this empirically
/// but it is currently set based on vibes. My thinking around 4 codepoints
/// is that most skin-tone emoji are <= 4 codepoints, letter combiners
/// are usually <= 4 codepoints, and 4 codepoints is a nice power of two
/// for alignment.
const grapheme_chunk_len = 4;
const grapheme_chunk = grapheme_chunk_len * @sizeOf(u21);
const GraphemeAlloc = BitmapAllocator(grapheme_chunk);
const grapheme_count_default = GraphemeAlloc.bitmap_bit_size;
const grapheme_bytes_default = grapheme_count_default * grapheme_chunk;
const GraphemeMap = AutoOffsetHashMap(Offset(Cell), Offset(u21).Slice);
/// A page represents a specific section of terminal screen. The primary
/// idea of a page is that it is a fully self-contained unit that can be
/// serialized, copied, etc. as a convenient way to represent a section
/// of the screen.
///
/// This property is useful for renderers which want to copy just the pages
/// for the visible portion of the screen, or for infinite scrollback where
/// we may want to serialize and store pages that are sufficiently far
/// away from the current viewport.
///
/// Pages are always backed by a single contiguous block of memory that is
/// aligned on a page boundary. This makes it easy and fast to copy pages
/// around. Within the contiguous block of memory, the contents of a page are
/// thoughtfully laid out to optimize primarily for terminal IO (VT streams)
/// and to minimize memory usage.
pub const Page = struct {
comptime {
// The alignment of our members. We want to ensure that the page
// alignment is always divisible by this.
assert(std.mem.page_size % @max(
@alignOf(Row),
@alignOf(Cell),
style.Set.base_align,
) == 0);
}
/// The backing memory for the page. A page is always made up of a
/// a single contiguous block of memory that is aligned on a page
/// boundary and is a multiple of the system page size.
memory: []align(std.mem.page_size) u8,
/// The array of rows in the page. The rows are always in row order
/// (i.e. index 0 is the top row, index 1 is the row below that, etc.)
rows: Offset(Row),
/// The array of cells in the page. The cells are NOT in row order,
/// but they are in column order. To determine the mapping of cells
/// to row, you must use the `rows` field. From the pointer to the
/// first column, all cells in that row are laid out in column order.
cells: Offset(Cell),
/// The multi-codepoint grapheme data for this page. This is where
/// any cell that has more than one codepoint will be stored. This is
/// relatively rare (typically only emoji) so this defaults to a very small
/// size and we force page realloc when it grows.
grapheme_alloc: GraphemeAlloc,
/// The mapping of cell to grapheme data. The exact mapping is the
/// cell offset to the grapheme data offset. Therefore, whenever a
/// cell is moved (i.e. `erase`) then the grapheme data must be updated.
/// Grapheme data is relatively rare so this is considered a slow
/// path.
grapheme_map: GraphemeMap,
/// The available set of styles in use on this page.
styles: style.Set,
/// The current dimensions of the page. The capacity may be larger
/// than this. This allows us to allocate a larger page than necessary
/// and also to resize a page smaller witout reallocating.
size: Size,
/// The capacity of this page. This is the full size of the backing
/// memory and is fixed at page creation time.
capacity: Capacity,
/// Initialize a new page, allocating the required backing memory.
/// The size of the initialized page defaults to the full capacity.
///
/// The backing memory is always allocated using mmap directly.
/// You cannot use custom allocators with this structure because
/// it is critical to performance that we use mmap.
pub fn init(cap: Capacity) !Page {
const l = layout(cap);
// We use mmap directly to avoid Zig allocator overhead
// (small but meaningful for this path) and because a private
// anonymous mmap is guaranteed on Linux and macOS to be zeroed,
// which is a critical property for us.
assert(l.total_size % std.mem.page_size == 0);
const backing = try std.os.mmap(
null,
l.total_size,
std.os.PROT.READ | std.os.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
errdefer std.os.munmap(backing);
const buf = OffsetBuf.init(backing);
return initBuf(buf, l);
}
/// Initialize a new page using the given backing memory.
/// It is up to the caller to not call deinit on these pages.
pub fn initBuf(buf: OffsetBuf, l: Layout) Page {
const cap = l.capacity;
const rows = buf.member(Row, l.rows_start);
const cells = buf.member(Cell, l.cells_start);
// We need to go through and initialize all the rows so that
// they point to a valid offset into the cells, since the rows
// zero-initialized aren't valid.
const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows];
for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| {
const start = y * cap.cols;
row.* = .{
.cells = getOffset(Cell, buf, &cells_ptr[start]),
};
}
return .{
.memory = @alignCast(buf.start()[0..l.total_size]),
.rows = rows,
.cells = cells,
.styles = style.Set.init(
buf.add(l.styles_start),
l.styles_layout,
),
.grapheme_alloc = GraphemeAlloc.init(
buf.add(l.grapheme_alloc_start),
l.grapheme_alloc_layout,
),
.grapheme_map = GraphemeMap.init(
buf.add(l.grapheme_map_start),
l.grapheme_map_layout,
),
.size = .{ .cols = cap.cols, .rows = cap.rows },
.capacity = cap,
};
}
/// Deinitialize the page, freeing any backing memory. Do NOT call
/// this if you allocated the backing memory yourself (i.e. you used
/// initBuf).
pub fn deinit(self: *Page) void {
std.os.munmap(self.memory);
self.* = undefined;
}
/// Clone the contents of this page. This will allocate new memory
/// using the page allocator. If you want to manage memory manually,
/// use cloneBuf.
pub fn clone(self: *const Page) !Page {
const backing = try std.os.mmap(
null,
self.memory.len,
std.os.PROT.READ | std.os.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
errdefer std.os.munmap(backing);
return self.cloneBuf(backing);
}
/// Clone the entire contents of this page.
///
/// The buffer must be at least the size of self.memory.
pub fn cloneBuf(self: *const Page, buf: []align(std.mem.page_size) u8) Page {
assert(buf.len >= self.memory.len);
// The entire concept behind a page is that everything is stored
// as offsets so we can do a simple linear copy of the backing
// memory and copy all the offsets and everything will work.
var result = self.*;
result.memory = buf[0..self.memory.len];
// This is a memcpy. We may want to investigate if there are
// faster ways to do this (i.e. copy-on-write tricks) but I suspect
// they'll be slower. I haven't experimented though.
// std.log.warn("copy bytes={}", .{self.memory.len});
fastmem.copy(u8, result.memory, self.memory);
return result;
}
/// Clone the contents of another page into this page. The capacities
/// can be different, but the size of the other page must fit into
/// this page.
///
/// The y_start and y_end parameters allow you to clone only a portion
/// of the other page. This is useful for splitting a page into two
/// or more pages.
///
/// The column count of this page will always be the same as this page.
/// If the other page has more columns, the extra columns will be
/// truncated. If the other page has fewer columns, the extra columns
/// will be zeroed.
///
/// The current page is assumed to be empty. We will not clear any
/// existing data in the current page.
pub fn cloneFrom(
self: *Page,
other: *const Page,
y_start: usize,
y_end: usize,
) !void {
assert(y_start <= y_end);
assert(y_end <= other.size.rows);
assert(y_end - y_start <= self.size.rows);
if (comptime std.debug.runtime_safety) {
// The current page must be empty.
assert(self.styles.count(self.memory) == 0);
assert(self.graphemeCount() == 0);
}
const other_rows = other.rows.ptr(other.memory)[y_start..y_end];
const rows = self.rows.ptr(self.memory)[0 .. y_end - y_start];
for (rows, other_rows) |*dst_row, *src_row| {
// Copy all the row metadata but keep our cells offset
const cells_offset = dst_row.cells;
dst_row.* = src_row.*;
dst_row.cells = cells_offset;
const cell_len = @min(self.size.cols, other.size.cols);
const other_cells = src_row.cells.ptr(other.memory)[0..cell_len];
const cells = dst_row.cells.ptr(self.memory)[0..cell_len];
// If we have no managed memory in the row, we can just copy.
if (!dst_row.grapheme and !dst_row.styled) {
fastmem.copy(Cell, cells, other_cells);
continue;
}
// We have managed memory, so we have to do a slower copy to
// get all of that right.
for (cells, other_cells) |*dst_cell, *src_cell| {
dst_cell.* = src_cell.*;
if (src_cell.hasGrapheme()) {
dst_cell.content_tag = .codepoint; // required for appendGrapheme
const cps = other.lookupGrapheme(src_cell).?;
for (cps) |cp| try self.appendGrapheme(dst_row, dst_cell, cp);
}
if (src_cell.style_id != style.default_id) {
const other_style = other.styles.lookupId(other.memory, src_cell.style_id).?.*;
const md = try self.styles.upsert(self.memory, other_style);
md.ref += 1;
dst_cell.style_id = md.id;
}
}
}
}
/// Get a single row. y must be valid.
pub fn getRow(self: *const Page, y: usize) *Row {
assert(y < self.size.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.size.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,
cell: *Cell,
} {
assert(y < self.size.rows);
assert(x < self.size.cols);
const rows = self.rows.ptr(self.memory);
const row = &rows[y];
const cell = &row.cells.ptr(self.memory)[x];
return .{ .row = row, .cell = cell };
}
/// Move a cell from one location to another. This will replace the
/// previous contents with a blank cell. Because this is a move, this
/// doesn't allocate and can't fail.
pub fn moveCells(
self: *Page,
src_row: *Row,
src_left: usize,
dst_row: *Row,
dst_left: usize,
len: usize,
) void {
const src_cells = src_row.cells.ptr(self.memory)[src_left .. src_left + len];
const dst_cells = dst_row.cells.ptr(self.memory)[dst_left .. dst_left + len];
// If src has no graphemes, this is very fast.
const src_grapheme = src_row.grapheme or grapheme: {
for (src_cells) |c| if (c.hasGrapheme()) break :grapheme true;
break :grapheme false;
};
if (!src_grapheme) {
fastmem.copy(Cell, dst_cells, src_cells);
return;
}
@panic("TODO: grapheme move");
}
/// Clear the cells in the given row. This will reclaim memory used
/// by graphemes and styles. Note that if the style cleared is still
/// active, Page cannot know this and it will still be ref counted down.
/// The best solution for this is to artificially increment the ref count
/// prior to calling this function.
pub fn clearCells(
self: *Page,
row: *Row,
left: usize,
end: usize,
) void {
const cells = row.cells.ptr(self.memory)[left..end];
if (row.grapheme) {
for (cells) |*cell| {
if (cell.hasGrapheme()) self.clearGrapheme(row, cell);
}
}
if (row.styled) {
for (cells) |*cell| {
if (cell.style_id == style.default_id) continue;
if (self.styles.lookupId(self.memory, cell.style_id)) |prev_style| {
// Below upsert can't fail because it should already be present
const md = self.styles.upsert(self.memory, prev_style.*) catch unreachable;
assert(md.ref > 0);
md.ref -= 1;
if (md.ref == 0) self.styles.remove(self.memory, cell.style_id);
}
}
if (cells.len == self.size.cols) row.styled = false;
}
@memset(cells, .{});
}
/// Append a codepoint to the given cell as a grapheme.
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
if (comptime std.debug.runtime_safety) assert(cell.hasText());
const cell_offset = getOffset(Cell, self.memory, cell);
var map = self.grapheme_map.map(self.memory);
// If this cell has no graphemes, we can go faster by knowing we
// need to allocate a new grapheme slice and update the map.
if (cell.content_tag != .codepoint_grapheme) {
const cps = try self.grapheme_alloc.alloc(u21, self.memory, 1);
errdefer self.grapheme_alloc.free(self.memory, cps);
cps[0] = cp;
try map.putNoClobber(cell_offset, .{
.offset = getOffset(u21, self.memory, @ptrCast(cps.ptr)),
.len = 1,
});
errdefer map.remove(cell_offset);
cell.content_tag = .codepoint_grapheme;
row.grapheme = true;
return;
}
// The cell already has graphemes. We need to append to the existing
// grapheme slice and update the map.
assert(row.grapheme);
const slice = map.getPtr(cell_offset).?;
// If our slice len doesn't divide evenly by the grapheme chunk
// length then we can utilize the additional chunk space.
if (slice.len % grapheme_chunk_len != 0) {
const cps = slice.offset.ptr(self.memory);
cps[slice.len] = cp;
slice.len += 1;
return;
}
// We are out of chunk space. There is no fast path here. We need
// to allocate a larger chunk. This is a very slow path. We expect
// most graphemes to fit within our chunk size.
const cps = try self.grapheme_alloc.alloc(u21, self.memory, slice.len + 1);
errdefer self.grapheme_alloc.free(self.memory, cps);
const old_cps = slice.offset.ptr(self.memory)[0..slice.len];
fastmem.copy(u21, cps[0..old_cps.len], old_cps);
cps[slice.len] = cp;
slice.* = .{
.offset = getOffset(u21, self.memory, @ptrCast(cps.ptr)),
.len = slice.len + 1,
};
// Free our old chunk
self.grapheme_alloc.free(self.memory, old_cps);
}
/// Returns the codepoints for the given cell. These are the codepoints
/// in addition to the first codepoint. The first codepoint is NOT
/// included since it is on the cell itself.
pub fn lookupGrapheme(self: *const Page, cell: *Cell) ?[]u21 {
const cell_offset = getOffset(Cell, self.memory, cell);
const map = self.grapheme_map.map(self.memory);
const slice = map.get(cell_offset) orelse return null;
return slice.offset.ptr(self.memory)[0..slice.len];
}
/// Clear the graphemes for a given cell.
pub fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void {
if (comptime std.debug.runtime_safety) assert(cell.hasGrapheme());
// Get our entry in the map, which must exist
const cell_offset = getOffset(Cell, self.memory, cell);
var map = self.grapheme_map.map(self.memory);
const entry = map.getEntry(cell_offset).?;
// Free our grapheme data
const cps = entry.value_ptr.offset.ptr(self.memory)[0..entry.value_ptr.len];
self.grapheme_alloc.free(self.memory, cps);
// Remove the entry
map.removeByPtr(entry.key_ptr);
// Mark that we no longer have graphemes, also search the row
// to make sure its state is correct.
cell.content_tag = .codepoint;
const cells = row.cells.ptr(self.memory)[0..self.size.cols];
for (cells) |c| if (c.hasGrapheme()) return;
row.grapheme = false;
}
/// Returns the number of graphemes in the page. This isn't the byte
/// size but the total number of unique cells that have grapheme data.
pub fn graphemeCount(self: *const Page) usize {
return self.grapheme_map.map(self.memory).count();
}
/// Move graphemes to another cell in the same row.
pub fn moveGraphemeWithinRow(self: *Page, src: *Cell, dst: *Cell) void {
// Note: we don't assert src has graphemes here because one of
// the places we call this is from insertBlanks where the cells have
// already swapped cell data but not grapheme data.
// Get our entry in the map, which must exist
const src_offset = getOffset(Cell, self.memory, src);
var map = self.grapheme_map.map(self.memory);
const entry = map.getEntry(src_offset).?;
const value = entry.value_ptr.*;
// Remove the entry so we know we have space
map.removeByPtr(entry.key_ptr);
// Add the entry for the new cell
const dst_offset = getOffset(Cell, self.memory, dst);
map.putAssumeCapacity(dst_offset, value);
}
pub const Layout = struct {
total_size: usize,
rows_start: usize,
rows_size: usize,
cells_start: usize,
cells_size: usize,
styles_start: usize,
styles_layout: style.Set.Layout,
grapheme_alloc_start: usize,
grapheme_alloc_layout: GraphemeAlloc.Layout,
grapheme_map_start: usize,
grapheme_map_layout: GraphemeMap.Layout,
capacity: Capacity,
};
/// The memory layout for a page given a desired minimum cols
/// and rows size.
pub fn layout(cap: Capacity) Layout {
const rows_count: usize = @intCast(cap.rows);
const rows_start = 0;
const rows_end: usize = rows_start + (rows_count * @sizeOf(Row));
const cells_count: usize = @intCast(cap.cols * cap.rows);
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
const styles_layout = style.Set.layout(cap.styles);
const styles_start = alignForward(usize, cells_end, style.Set.base_align);
const styles_end = styles_start + styles_layout.total_size;
const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes);
const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align);
const grapheme_alloc_end = grapheme_alloc_start + grapheme_alloc_layout.total_size;
const grapheme_count = @divFloor(cap.grapheme_bytes, grapheme_chunk);
const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count));
const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align);
const grapheme_map_end = grapheme_map_start + grapheme_map_layout.total_size;
const total_size = alignForward(usize, grapheme_map_end, std.mem.page_size);
return .{
.total_size = total_size,
.rows_start = rows_start,
.rows_size = rows_end - rows_start,
.cells_start = cells_start,
.cells_size = cells_end - cells_start,
.styles_start = styles_start,
.styles_layout = styles_layout,
.grapheme_alloc_start = grapheme_alloc_start,
.grapheme_alloc_layout = grapheme_alloc_layout,
.grapheme_map_start = grapheme_map_start,
.grapheme_map_layout = grapheme_map_layout,
.capacity = cap,
};
}
};
/// The standard capacity for a page that doesn't have special
/// requirements. This is enough to support a very large number of cells.
/// The standard capacity is chosen as the fast-path for allocation.
pub const std_capacity: Capacity = .{
.cols = 215,
.rows = 215,
.styles = 128,
.grapheme_bytes = 8192,
};
/// The size of this page.
pub const Size = struct {
cols: size.CellCountInt,
rows: size.CellCountInt,
};
/// Capacity of this page.
pub const Capacity = struct {
/// Number of columns and rows we can know about.
cols: size.CellCountInt,
rows: size.CellCountInt,
/// Number of unique styles that can be used on this page.
styles: u16 = 16,
/// Number of bytes to allocate for grapheme data.
grapheme_bytes: usize = grapheme_bytes_default,
pub const Adjustment = struct {
cols: ?size.CellCountInt = null,
};
/// Adjust the capacity parameters while retaining the same total size.
/// Adjustments always happen by limiting the rows in the page. Everything
/// else can grow. If it is impossible to achieve the desired adjustment,
/// OutOfMemory is returned.
pub fn adjust(self: Capacity, req: Adjustment) Allocator.Error!Capacity {
var adjusted = self;
if (req.cols) |cols| {
// The math below only works if there is no alignment gap between
// the end of the rows array and the start of the cells array.
//
// To guarantee this, we assert that Row's size is a multiple of
// Cell's alignment, so that any length array of Rows will end on
// a valid alignment for the start of the Cell array.
assert(@sizeOf(Row) % @alignOf(Cell) == 0);
const layout = Page.layout(self);
// In order to determine the amount of space in the page available
// for rows & cells (which will allow us to calculate the number of
// rows we can fit at a certain column width) we need to layout the
// "meta" members of the page (i.e. everything else) from the end.
const grapheme_map_start = alignBackward(
usize,
layout.total_size - layout.grapheme_map_layout.total_size,
GraphemeMap.base_align
);
const grapheme_alloc_start = alignBackward(
usize,
grapheme_map_start - layout.grapheme_alloc_layout.total_size,
GraphemeAlloc.base_align
);
const styles_start = alignBackward(
usize,
grapheme_alloc_start - layout.styles_layout.total_size,
style.Set.base_align
);
const available_size = styles_start;
const size_per_row = @sizeOf(Row) + (@sizeOf(Cell) * @as(usize, @intCast(cols)));
const new_rows = @divFloor(available_size, size_per_row);
// If our rows go to zero then we can't fit any row metadata
// for the desired number of columns.
if (new_rows == 0) return error.OutOfMemory;
adjusted.cols = cols;
adjusted.rows = @intCast(new_rows);
}
return adjusted;
}
};
pub const Row = packed struct(u64) {
/// The cells in the row offset from the page.
cells: Offset(Cell),
/// True if this row is soft-wrapped. The first cell of the next
/// row is a continuation of this row.
wrap: bool = false,
/// True if the previous row to this one is soft-wrapped and
/// this row is a continuation of that row.
wrap_continuation: bool = false,
/// True if any of the cells in this row have multi-codepoint
/// grapheme clusters. If this is true, some fast paths are not
/// possible because erasing for example may need to clear existing
/// grapheme data.
grapheme: bool = false,
/// True if any of the cells in this row have a ref-counted style.
/// This can have false positives but never a false negative. Meaning:
/// this will be set to true the first time a style is used, but it
/// will not be set to false if the style is no longer used, because
/// checking for that condition is too expensive.
///
/// Why have this weird false positive flag at all? This makes VT operations
/// that erase cells (such as insert lines, delete lines, erase chars,
/// etc.) MUCH MUCH faster in the case that the row was never styled.
/// At the time of writing this, the speed difference is around 4x.
styled: bool = false,
/// The semantic prompt type for this row as specified by the
/// running program, or "unknown" if it was never set.
semantic_prompt: SemanticPrompt = .unknown,
_padding: u25 = 0,
/// Semantic prompt type.
pub const SemanticPrompt = enum(u3) {
/// Unknown, the running application didn't tell us for this line.
unknown = 0,
/// This is a prompt line, meaning it only contains the shell prompt.
/// For poorly behaving shells, this may also be the input.
prompt = 1,
prompt_continuation = 2,
/// This line contains the input area. We don't currently track
/// where this actually is in the line, so we just assume it is somewhere.
input = 3,
/// This line is the start of command output.
command = 4,
/// True if this is a prompt or input line.
pub fn promptOrInput(self: SemanticPrompt) bool {
return self == .prompt or self == .prompt_continuation or self == .input;
}
};
};
/// A cell represents a single terminal grid cell.
///
/// The zero value of this struct must be a valid cell representing empty,
/// since we zero initialize the backing memory for a page.
pub const Cell = packed struct(u64) {
/// The content tag dictates the active tag in content and possibly
/// some other behaviors.
content_tag: ContentTag = .codepoint,
/// The content of the cell. This is a union based on content_tag.
content: packed union {
/// The codepoint that this cell contains. If `grapheme` is false,
/// then this is the only codepoint in the cell. If `grapheme` is
/// true, then this is the first codepoint in the grapheme cluster.
codepoint: u21,
/// The content is an empty cell with a background color.
color_palette: u8,
color_rgb: RGB,
} = .{ .codepoint = 0 },
/// The style ID to use for this cell within the style map. Zero
/// is always the default style so no lookup is required.
style_id: style.Id = 0,
/// The wide property of this cell, for wide characters. Characters in
/// a terminal grid can only be 1 or 2 cells wide. A wide character
/// is always next to a spacer. This is used to determine both the width
/// and spacer properties of a cell.
wide: Wide = .narrow,
/// Whether this was written with the protection flag set.
protected: bool = false,
_padding: u19 = 0,
pub const ContentTag = enum(u2) {
/// A single codepoint, could be zero to be empty cell.
codepoint = 0,
/// A codepoint that is part of a multi-codepoint grapheme cluster.
/// The codepoint tag is active in content, but also expect more
/// codepoints in the grapheme data.
codepoint_grapheme = 1,
/// The cell has no text but only a background color. This is an
/// optimization so that cells with only backgrounds don't take up
/// style map space and also don't require a style map lookup.
bg_color_palette = 2,
bg_color_rgb = 3,
};
pub const RGB = packed struct {
r: u8,
g: u8,
b: u8,
};
pub const Wide = enum(u2) {
/// Not a wide character, cell width 1.
narrow = 0,
/// Wide character, cell width 2.
wide = 1,
/// Spacer after wide character. Do not render.
spacer_tail = 2,
/// Spacer at the end of a soft-wrapped line to indicate that a wide
/// character is continued on the next line.
spacer_head = 3,
};
/// Helper to make a cell that just has a codepoint.
pub fn init(cp: u21) Cell {
return .{
.content_tag = .codepoint,
.content = .{ .codepoint = cp },
};
}
pub fn hasText(self: Cell) bool {
return switch (self.content_tag) {
.codepoint,
.codepoint_grapheme,
=> self.content.codepoint != 0,
.bg_color_palette,
.bg_color_rgb,
=> false,
};
}
pub fn codepoint(self: Cell) u21 {
return switch (self.content_tag) {
.codepoint,
.codepoint_grapheme,
=> self.content.codepoint,
.bg_color_palette,
.bg_color_rgb,
=> 0,
};
}
/// The width in grid cells that this cell takes up.
pub fn gridWidth(self: Cell) u2 {
return switch (self.wide) {
.narrow, .spacer_head, .spacer_tail => 1,
.wide => 2,
};
}
pub fn hasStyling(self: Cell) bool {
return self.style_id != style.default_id;
}
/// Returns true if the cell has no text or styling.
pub fn isEmpty(self: Cell) bool {
return switch (self.content_tag) {
// Textual cells are empty if they have no text and are narrow.
// The "narrow" requirement is because wide spacers are meaningful.
.codepoint,
.codepoint_grapheme,
=> !self.hasText() and self.wide == .narrow,
.bg_color_palette,
.bg_color_rgb,
=> false,
};
}
pub fn hasGrapheme(self: Cell) bool {
return self.content_tag == .codepoint_grapheme;
}
/// Returns true if the set of cells has text in it.
pub fn hasTextAny(cells: []const Cell) bool {
for (cells) |cell| {
if (cell.hasText()) return true;
}
return false;
}
};
// Uncomment this when you want to do some math.
// test "Page size calculator" {
// const total_size = alignForward(
// usize,
// Page.layout(.{
// .cols = 250,
// .rows = 250,
// .styles = 128,
// .grapheme_bytes = 1024,
// }).total_size,
// std.mem.page_size,
// );
//
// std.log.warn("total_size={} pages={}", .{
// total_size,
// total_size / std.mem.page_size,
// });
// }
//
// test "Page std size" {
// // We want to ensure that the standard capacity is what we
// // expect it to be. Changing this is fine but should be done with care
// // so we fail a test if it changes.
// const total_size = Page.layout(std_capacity).total_size;
// try testing.expectEqual(@as(usize, 524_288), total_size); // 512 KiB
// //const pages = total_size / std.mem.page_size;
// }
test "Page capacity adjust cols down" {
const original = std_capacity;
const original_size = Page.layout(original).total_size;
const adjusted = try original.adjust(.{ .cols = original.cols / 2 });
const adjusted_size = Page.layout(adjusted).total_size;
try testing.expectEqual(original_size, adjusted_size);
// If we layout a page with 1 more row and it's still the same size
// then adjust is not producing enough rows.
var bigger = adjusted;
bigger.rows += 1;
const bigger_size = Page.layout(bigger).total_size;
try testing.expect(bigger_size > original_size);
}
test "Page capacity adjust cols down to 1" {
const original = std_capacity;
const original_size = Page.layout(original).total_size;
const adjusted = try original.adjust(.{ .cols = 1 });
const adjusted_size = Page.layout(adjusted).total_size;
try testing.expectEqual(original_size, adjusted_size);
// If we layout a page with 1 more row and it's still the same size
// then adjust is not producing enough rows.
var bigger = adjusted;
bigger.rows += 1;
const bigger_size = Page.layout(bigger).total_size;
try testing.expect(bigger_size > original_size);
}
test "Page capacity adjust cols up" {
const original = std_capacity;
const original_size = Page.layout(original).total_size;
const adjusted = try original.adjust(.{ .cols = original.cols * 2 });
const adjusted_size = Page.layout(adjusted).total_size;
try testing.expectEqual(original_size, adjusted_size);
// If we layout a page with 1 more row and it's still the same size
// then adjust is not producing enough rows.
var bigger = adjusted;
bigger.rows += 1;
const bigger_size = Page.layout(bigger).total_size;
try testing.expect(bigger_size > original_size);
}
test "Page capacity adjust cols sweep" {
var cap = std_capacity;
const original_cols = cap.cols;
const original_size = Page.layout(cap).total_size;
for (1..original_cols*2) |c| {
cap = try cap.adjust(.{ .cols = @as(u16, @intCast(c)) });
const adjusted_size = Page.layout(cap).total_size;
try testing.expectEqual(original_size, adjusted_size);
// If we layout a page with 1 more row and it's still the same size
// then adjust is not producing enough rows.
var bigger = cap;
bigger.rows += 1;
const bigger_size = Page.layout(bigger).total_size;
try testing.expect(bigger_size > original_size);
}
}
test "Page capacity adjust cols too high" {
const original = std_capacity;
try testing.expectError(
error.OutOfMemory,
original.adjust(.{ .cols = std.math.maxInt(size.CellCountInt) }),
);
}
test "Page init" {
var page = try Page.init(.{
.cols = 120,
.rows = 80,
.styles = 32,
});
defer page.deinit();
}
test "Page read and write cells" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(y) },
};
}
// Read it again
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
}
test "Page appendGrapheme small" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
const rac = page.getRowAndCell(0, 0);
rac.cell.* = Cell.init(0x09);
// One
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
try testing.expect(rac.row.grapheme);
try testing.expect(rac.cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0x0A}, page.lookupGrapheme(rac.cell).?);
// Two
try page.appendGrapheme(rac.row, rac.cell, 0x0B);
try testing.expect(rac.row.grapheme);
try testing.expect(rac.cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{ 0x0A, 0x0B }, page.lookupGrapheme(rac.cell).?);
// Clear it
page.clearGrapheme(rac.row, rac.cell);
try testing.expect(!rac.row.grapheme);
try testing.expect(!rac.cell.hasGrapheme());
}
test "Page appendGrapheme larger than chunk" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
const rac = page.getRowAndCell(0, 0);
rac.cell.* = Cell.init(0x09);
const count = grapheme_chunk_len * 10;
for (0..count) |i| {
try page.appendGrapheme(rac.row, rac.cell, @intCast(0x0A + i));
}
const cps = page.lookupGrapheme(rac.cell).?;
try testing.expectEqual(@as(usize, count), cps.len);
for (0..count) |i| {
try testing.expectEqual(@as(u21, @intCast(0x0A + i)), cps[i]);
}
}
test "Page clearGrapheme not all cells" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
const rac = page.getRowAndCell(0, 0);
rac.cell.* = Cell.init(0x09);
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
const rac2 = page.getRowAndCell(1, 0);
rac2.cell.* = Cell.init(0x09);
try page.appendGrapheme(rac2.row, rac2.cell, 0x0A);
// Clear it
page.clearGrapheme(rac.row, rac.cell);
try testing.expect(rac.row.grapheme);
try testing.expect(!rac.cell.hasGrapheme());
try testing.expect(rac2.cell.hasGrapheme());
}
test "Page clone" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(y) },
};
}
// Clone
var page2 = try page.clone();
defer page2.deinit();
try testing.expectEqual(page2.capacity, page.capacity);
// Read it again
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
// Write again
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 0 },
};
}
// Read it again, should be unchanged
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
// Read the original
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
}
}
test "Page cloneFrom" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(y) },
};
}
// Clone
var page2 = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page2.deinit();
try page2.cloneFrom(&page, 0, page.size.rows);
// Read it again
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
// Write again
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 0 },
};
}
// Read it again, should be unchanged
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
// Read the original
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
}
}
test "Page cloneFrom shrink columns" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(y) },
};
}
// Clone
var page2 = try Page.init(.{
.cols = 5,
.rows = 10,
.styles = 8,
});
defer page2.deinit();
try page2.cloneFrom(&page, 0, page.size.rows);
try testing.expectEqual(@as(size.CellCountInt, 5), page2.size.cols);
// Read it again
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
}
test "Page cloneFrom partial" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(y) },
};
}
// Clone
var page2 = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page2.deinit();
try page2.cloneFrom(&page, 0, 5);
// Read it again
for (0..5) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
}
for (5..page2.size.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
}
}
test "Page cloneFrom graphemes" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(y + 1) },
};
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
}
// Clone
var page2 = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page2.deinit();
try page2.cloneFrom(&page, 0, page.size.rows);
// Read it again
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y + 1)), rac.cell.content.codepoint);
try testing.expect(rac.row.grapheme);
try testing.expect(rac.cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0x0A}, page2.lookupGrapheme(rac.cell).?);
}
// Write again
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
page.clearGrapheme(rac.row, rac.cell);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 0 },
};
}
// Read it again, should be unchanged
for (0..page2.capacity.rows) |y| {
const rac = page2.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, @intCast(y + 1)), rac.cell.content.codepoint);
try testing.expect(rac.row.grapheme);
try testing.expect(rac.cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0x0A}, page2.lookupGrapheme(rac.cell).?);
}
// Read the original
for (0..page.capacity.rows) |y| {
const rac = page.getRowAndCell(1, y);
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
}
}