diff --git a/Cargo.lock b/Cargo.lock index ff84597..7a033a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -872,6 +872,7 @@ dependencies = [ "ghostty-os", "log", "ratatui", + "tempfile", "thiserror 2.0.21", ] @@ -916,6 +917,7 @@ version = "0.1.0" dependencies = [ "bitflags 2.13.2", "freetype-sys", + "ghostty-datastruct", "ghostty-unicode", "harfbuzz-sys", "log", diff --git a/crates/ghostty-terminal/Cargo.toml b/crates/ghostty-terminal/Cargo.toml index 621a33e..1f692ae 100644 --- a/crates/ghostty-terminal/Cargo.toml +++ b/crates/ghostty-terminal/Cargo.toml @@ -6,8 +6,6 @@ license.workspace = true [dependencies] ghostty-vt.workspace = true -ghostty-unicode.workspace = true -ghostty-datastruct.workspace = true [lints] workspace = true diff --git a/crates/ghostty-terminal/src/hyperlink.rs b/crates/ghostty-terminal/src/hyperlink.rs new file mode 100644 index 0000000..a41e4ab --- /dev/null +++ b/crates/ghostty-terminal/src/hyperlink.rs @@ -0,0 +1,43 @@ +//! Owned OSC 8 hyperlinks and per-page identifiers. +/// Nonzero page-local hyperlink identifier. +pub type Id = u16; +/// Mapping from a cell index to its hyperlink. +pub type Map = std::collections::HashMap; +/// An explicit OSC 8 ID or an automatically generated screen-local ID. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum HyperlinkId { + /// Application-provided byte string. + Explicit(Vec), + /// Screen-generated identifier. + Implicit(u32), +} +/// A decoded hyperlink owning its ID and URI. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Hyperlink { + /// Hyperlink identity. + pub id: HyperlinkId, + /// URI bytes, which need not be UTF-8. + pub uri: Vec, +} +impl Hyperlink { + /// Duplicate the owned strings. + pub fn dupe(&self) -> Self { + self.clone() + } + /// Compare identity and URI. + pub fn eql(&self, other: &Self) -> bool { + self == other + } + /// Logical string allocation including 32-byte chunk rounding. + pub fn string_bytes(&self) -> usize { + self.uri.len().next_multiple_of(32) + + match &self.id { + HyperlinkId::Explicit(v) => v.len().next_multiple_of(32), + HyperlinkId::Implicit(_) => 0, + } + } +} +/// Owned page entry; unlike Zig it needs no base address for decoding. +pub type PageEntry = Hyperlink; +/// Reference-counted set of page entries. +pub type Set = crate::ref_counted_set::RefCountedSet; diff --git a/crates/ghostty-terminal/src/lib.rs b/crates/ghostty-terminal/src/lib.rs index a740c78..6dfcc74 100644 --- a/crates/ghostty-terminal/src/lib.rs +++ b/crates/ghostty-terminal/src/lib.rs @@ -1 +1,9 @@ -//! ghostty-terminal: see docs/rewrite/ARCHITECTURE.md for the module map. +//! Safe page-based terminal memory model. +#![cfg_attr(test, allow(clippy::unwrap_used))] +pub mod hyperlink; +pub mod page; +pub mod page_list; +pub mod point; +pub mod ref_counted_set; +pub mod size; +pub mod style; diff --git a/crates/ghostty-terminal/src/page.rs b/crates/ghostty-terminal/src/page.rs new file mode 100644 index 0000000..aad9e7c --- /dev/null +++ b/crates/ghostty-terminal/src/page.rs @@ -0,0 +1,2216 @@ +//! Compact cells and rows, with bounded owned page storage. +use crate::{ + hyperlink::{self, Hyperlink, HyperlinkId}, + ref_counted_set::{AddError, Layout as SetLayout}, + style::{self, Style}, +}; +use ghostty_vt::color::Rgb; +use std::collections::{HashMap, HashSet}; +/// Default grapheme allocation request, in bytes. +pub const GRAPHEME_BYTES_DEFAULT: u32 = 1024; +/// Default string allocation request, in bytes. +pub const STRING_BYTES_DEFAULT: u32 = 2048; +/// Reference ABI size of a hyperlink set item. +pub const HYPERLINK_ITEM_SIZE: usize = 48; +/// Reference ABI size of a style set item. +pub const STYLE_ITEM_SIZE: usize = 36; +/// The standard page capacity (smaller grapheme region in tests, as in Zig). +pub const STD_CAPACITY: Capacity = Capacity { + cols: 215, + rows: 215, + styles: 128, + hyperlink_bytes: 192, + grapheme_bytes: if cfg!(test) { 512 } else { 8192 }, + string_bytes: STRING_BYTES_DEFAULT, +}; +/// ContentTag field of the packed grid representation. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ContentTag { + /// Single codepoint. + #[default] + Codepoint = 0, + /// First codepoint of a grapheme cluster. + CodepointGrapheme = 1, + /// Background-only palette color. + BgColorPalette = 2, + /// Background-only RGB color. + BgColorRgb = 3, +} +/// Wide field of the packed grid representation. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Wide { + /// Single grid cell. + #[default] + Narrow = 0, + /// Two-cell character. + Wide = 1, + /// Trailing half of a wide character. + SpacerTail = 2, + /// End-of-line spacer before a wrapped wide character. + SpacerHead = 3, +} +/// SemanticContent field of the packed grid representation. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SemanticContent { + /// Ordinary command output. + #[default] + Output = 0, + /// User input. + Input = 1, + /// Application prompt. + Prompt = 2, +} +/// SemanticPrompt field of the packed grid representation. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SemanticPrompt { + /// No prompt cells. + #[default] + None = 0, + /// Primary prompt row. + Prompt = 1, + /// Continuation prompt row. + PromptContinuation = 2, +} +/// Eight-byte terminal cell; all-zero bits describe an empty cell. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct Cell(u64); +impl Cell { + /// Construct an unstyled single-codepoint cell. + pub fn init(cp: u32) -> Self { + let mut c = Self::default(); + c.set_codepoint(cp); + c + } + fn bits(self, shift: u32, mask: u64) -> u64 { + (self.0 >> shift) & mask + } + fn set_bits(&mut self, shift: u32, mask: u64, value: u64) { + self.0 = (self.0 & !(mask << shift)) | ((value & mask) << shift); + } + /// Return the bit-packed representation. + pub const fn raw(self) -> u64 { + self.0 + } + /// True when every bit is zero. + pub fn is_zero(self) -> bool { + self.0 == 0 + } + /// Return the codepoint, or zero for background-only cells. + pub fn codepoint(self) -> u32 { + if (self.content_tag() as u8) < 2 { + self.bits(2, 0x1fffff) as u32 + } else { + 0 + } + } + /// Set the codepoint payload without changing the content tag. + pub fn set_codepoint(&mut self, cp: u32) { + self.set_bits(2, 0xffffff, u64::from(cp & 0x1fffff)); + } + /// Return the palette payload. + pub fn color_palette(self) -> u8 { + self.bits(2, 255) as u8 + } + /// Set the palette payload without changing the content tag. + pub fn set_color_palette(&mut self, v: u8) { + self.set_bits(2, 0xffffff, u64::from(v)); + } + /// Return the RGB payload. + pub fn color_rgb(self) -> Rgb { + Rgb { + r: self.bits(2, 255) as u8, + g: self.bits(10, 255) as u8, + b: self.bits(18, 255) as u8, + } + } + /// Set the RGB payload without changing the content tag. + pub fn set_color_rgb(&mut self, v: Rgb) { + self.set_bits( + 2, + 0xffffff, + u64::from(v.r) | (u64::from(v.g) << 8) | (u64::from(v.b) << 16), + ); + } + /// True when the cell contains a nonzero text codepoint. + pub fn has_text(self) -> bool { + self.codepoint() != 0 + } + /// True when the cell refers to extra grapheme codepoints. + pub fn has_grapheme(self) -> bool { + self.content_tag() == ContentTag::CodepointGrapheme + } + /// Width in grid cells, including one-cell spacers. + pub fn grid_width(self) -> u8 { + if self.wide() == Wide::Wide { 2 } else { 1 } + } + /// True when the cell has a nondefault style ID. + pub fn has_styling(self) -> bool { + self.style_id() != 0 + } + /// True for a blank narrow textual cell; style does not affect this test. + pub fn is_empty(self) -> bool { + (self.content_tag() as u8) < 2 && !self.has_text() && self.wide() == Wide::Narrow + } + /// True if any cell has text. + pub fn has_text_any(cells: &[Self]) -> bool { + cells.iter().any(|c| c.has_text()) + } + /// Read the content_tag field. + pub fn content_tag(self) -> ContentTag { + match self.bits(0, 3) { + 0 => ContentTag::Codepoint, + 1 => ContentTag::CodepointGrapheme, + 2 => ContentTag::BgColorPalette, + _ => ContentTag::BgColorRgb, + } + } + /// Update the content_tag field. + pub fn set_content_tag(&mut self, v: ContentTag) { + self.set_bits(0, 3, v as u64); + } + /// Read the style_id field. + pub fn style_id(self) -> u16 { + self.bits(26, 65535) as u16 + } + /// Update the style_id field. + pub fn set_style_id(&mut self, v: u16) { + self.set_bits(26, 65535, v as u64); + } + /// Read the wide field. + pub fn wide(self) -> Wide { + match self.bits(42, 3) { + 0 => Wide::Narrow, + 1 => Wide::Wide, + 2 => Wide::SpacerTail, + _ => Wide::SpacerHead, + } + } + /// Update the wide field. + pub fn set_wide(&mut self, v: Wide) { + self.set_bits(42, 3, v as u64); + } + /// Read the protected field. + pub fn protected(self) -> bool { + self.bits(44, 1) != 0 + } + /// Update the protected field. + pub fn set_protected(&mut self, v: bool) { + self.set_bits(44, 1, v as u64); + } + /// Read the hyperlink field. + pub fn hyperlink(self) -> bool { + self.bits(45, 1) != 0 + } + /// Update the hyperlink field. + pub fn set_hyperlink(&mut self, v: bool) { + self.set_bits(45, 1, v as u64); + } + /// Read the semantic_content field. + pub fn semantic_content(self) -> SemanticContent { + match self.bits(46, 3) { + 0 => SemanticContent::Output, + 1 => SemanticContent::Input, + _ => SemanticContent::Prompt, + } + } + /// Update the semantic_content field. + pub fn set_semantic_content(&mut self, v: SemanticContent) { + self.set_bits(46, 3, v as u64); + } +} +/// Eight-byte row metadata. The low 32 bits hold its first cell index. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct Row(u64); +impl Row { + /// First cell index in the page's row-major storage. + pub fn cells(self) -> usize { + (self.0 & 0xffffffff) as usize + } + /// Set the row's first cell index. + pub fn set_cells(&mut self, v: usize) { + self.0 = (self.0 & !0xffffffff) | (v as u64 & 0xffffffff); + } + /// True if cells can have managed style, hyperlink, or grapheme data. + pub fn managed_memory(self) -> bool { + self.styled() || self.hyperlink() || self.grapheme() + } + /// Prompt-row classification. + pub fn semantic_prompt(self) -> SemanticPrompt { + match (self.0 >> 37) & 3 { + 1 => SemanticPrompt::Prompt, + 2 => SemanticPrompt::PromptContinuation, + _ => SemanticPrompt::None, + } + } + /// Set prompt-row classification. + pub fn set_semantic_prompt(&mut self, v: SemanticPrompt) { + self.0 = (self.0 & !(3 << 37)) | ((v as u64) << 37); + } + /// Read the wrap flag. + pub fn wrap(self) -> bool { + self.0 & (1 << 32) != 0 + } + /// Update the wrap flag. + pub fn set_wrap(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 32)) | (u64::from(v) << 32); + } + /// Read the wrap_continuation flag. + pub fn wrap_continuation(self) -> bool { + self.0 & (1 << 33) != 0 + } + /// Update the wrap_continuation flag. + pub fn set_wrap_continuation(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 33)) | (u64::from(v) << 33); + } + /// Read the grapheme flag. + pub fn grapheme(self) -> bool { + self.0 & (1 << 34) != 0 + } + /// Update the grapheme flag. + pub fn set_grapheme(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 34)) | (u64::from(v) << 34); + } + /// Read the styled flag. + pub fn styled(self) -> bool { + self.0 & (1 << 35) != 0 + } + /// Update the styled flag. + pub fn set_styled(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 35)) | (u64::from(v) << 35); + } + /// Read the hyperlink flag. + pub fn hyperlink(self) -> bool { + self.0 & (1 << 36) != 0 + } + /// Update the hyperlink flag. + pub fn set_hyperlink(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 36)) | (u64::from(v) << 36); + } + /// Read the kitty_virtual_placeholder flag. + pub fn kitty_virtual_placeholder(self) -> bool { + self.0 & (1 << 39) != 0 + } + /// Update the kitty_virtual_placeholder flag. + pub fn set_kitty_virtual_placeholder(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 39)) | (u64::from(v) << 39); + } + /// Read the dirty flag. + pub fn dirty(self) -> bool { + self.0 & (1 << 40) != 0 + } + /// Update the dirty flag. + pub fn set_dirty(&mut self, v: bool) { + self.0 = (self.0 & !(1 << 40)) | (u64::from(v) << 40); + } +} +/// Current logical page dimensions. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Size { + /// Columns currently addressable. + pub cols: u16, + /// Rows currently addressable. + pub rows: u16, +} +/// Fixed logical allocation limits for a page. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Capacity { + /// Allocated columns. + pub cols: u16, + /// Allocated rows. + pub rows: u16, + /// Requested style hash-table slots. + pub styles: u16, + /// Requested hyperlink-set bytes, excluding strings and map. + pub hyperlink_bytes: u16, + /// Requested grapheme bytes. + pub grapheme_bytes: u32, + /// Requested string bytes. + pub string_bytes: u32, +} +impl Default for Capacity { + fn default() -> Self { + Self { + cols: 0, + rows: 0, + styles: 16, + hyperlink_bytes: 192, + grapheme_bytes: GRAPHEME_BYTES_DEFAULT, + string_bytes: STRING_BYTES_DEFAULT, + } + } +} +/// Capacity adjustment that retains the original total byte allocation. +#[derive(Debug, Default, Clone, Copy)] +pub struct Adjustment { + /// New column count; rows absorb the size change. + pub cols: Option, +} +impl Capacity { + fn available_bytes_for_grid(self) -> usize { + let l = Page::layout(self); + let mut end = l.total_size; + for (bytes, align) in [ + (l.hyperlink_map_layout.total_size, 4), + (l.hyperlink_set_layout.total_size, 8), + (l.string_alloc_layout.total_size, 8), + (l.grapheme_map_layout.total_size, 8), + (l.grapheme_alloc_layout.total_size, 8), + (l.styles_layout.total_size, 8), + ] { + end = (end - bytes) & !(align - 1); + } + end + } + /// Maximum columns that still allow one row without enlarging the page. + pub fn max_cols(self) -> Option { + let bytes = self.available_bytes_for_grid(); + (bytes > 8).then(|| ((bytes - 8) / 8).min(65535) as u16) + } + /// Adjust columns, retaining the same page size by changing row capacity. + pub fn adjust(self, req: Adjustment) -> Result { + let Some(cols) = req.cols else { + return Ok(self); + }; + let rows = self.available_bytes_for_grid() / (8 + usize::from(cols) * 8); + if rows == 0 { + return Err(Error::OutOfMemory); + } + Ok(Self { + cols, + rows: rows.min(65535) as u16, + ..self + }) + } +} +/// Logical bitmap allocator layout, preserving the reference size formula. +#[derive(Debug, Default, Clone, Copy)] +pub struct AllocLayout { + /// Total bytes including the reference allocator's chunk multiplier. + pub total_size: usize, + /// Number of 64-bit chunk bitmaps. + pub bitmap_count: usize, + /// Start of allocation data. + pub chunks_start: usize, +} +impl AllocLayout { + fn new(cap: usize, chunk: usize) -> Self { + let aligned = cap.next_multiple_of(chunk); + let bitmap_count = (aligned / chunk).div_ceil(64); + Self { + total_size: bitmap_count * 8 + aligned * chunk, + bitmap_count, + chunks_start: bitmap_count * 8, + } + } +} +/// Logical offset hash-map layout. +#[derive(Debug, Default, Clone, Copy)] +pub struct MapLayout { + /// Total bytes including the 16-byte header. + pub total_size: usize, + /// Maximum entries (no extra load-factor reduction). + pub capacity: usize, +} +impl MapLayout { + fn new(capacity: usize, value_size: usize, value_align: usize) -> Self { + let keys_start = (16 + capacity).next_multiple_of(4); + let vals_start = (keys_start + capacity * 4).next_multiple_of(value_align); + Self { + total_size: (vals_start + capacity * value_size).next_multiple_of(value_align.max(4)), + capacity, + } + } +} +/// Complete logical page layout for the 64-bit Linux reference ABI. +#[derive(Debug, Clone, Copy)] +pub struct Layout { + /// Page-aligned byte size. + pub total_size: usize, + /// Row array offset. + pub rows_start: usize, + /// Row array bytes. + pub rows_size: usize, + /// Cell array offset. + pub cells_start: usize, + /// Cell array bytes. + pub cells_size: usize, + /// Style set offset. + pub styles_start: usize, + /// Style set layout. + pub styles_layout: SetLayout, + /// Grapheme allocator offset. + pub grapheme_alloc_start: usize, + /// Grapheme allocator layout. + pub grapheme_alloc_layout: AllocLayout, + /// Grapheme map offset. + pub grapheme_map_start: usize, + /// Grapheme map layout. + pub grapheme_map_layout: MapLayout, + /// String allocator offset. + pub string_alloc_start: usize, + /// String allocator layout. + pub string_alloc_layout: AllocLayout, + /// Hyperlink set offset. + pub hyperlink_set_start: usize, + /// Hyperlink set layout. + pub hyperlink_set_layout: SetLayout, + /// Hyperlink map offset. + pub hyperlink_map_start: usize, + /// Hyperlink map layout. + pub hyperlink_map_layout: MapLayout, + /// Requested capacity. + pub capacity: Capacity, +} +/// Recoverable page operation errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + /// Allocation or grid capacity exhausted. + OutOfMemory, + /// Cell or row is outside the logical page. + InvalidCoordinate, + /// A grid dimension is zero. + InvalidDimensions, + /// Style set needs more space. + StyleSetOutOfMemory, + /// Style IDs need compaction. + StyleSetNeedsRehash, + /// No contiguous grapheme allocation fits. + GraphemeAllocOutOfMemory, + /// Grapheme map capacity exhausted. + GraphemeMapOutOfMemory, + /// Hyperlink map capacity exhausted. + HyperlinkMapOutOfMemory, + /// Hyperlink set capacity exhausted. + SetOutOfMemory, + /// Hyperlink IDs need compaction. + SetNeedsRehash, + /// No contiguous string allocation fits. + StringsOutOfMemory, + /// Missing style or hyperlink ID. + InvalidId, + /// Reference count overflow or underflow. + InvalidRefCount, + /// Capacity cannot grow further. + OutOfSpace, +} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} +impl std::error::Error for Error {} +impl From for Error { + fn from(e: AddError) -> Self { + match e { + AddError::OutOfMemory => Self::StyleSetOutOfMemory, + AddError::NeedsRehash => Self::StyleSetNeedsRehash, + AddError::InvalidId => Self::InvalidId, + AddError::InvalidRefCount => Self::InvalidRefCount, + } + } +} +#[derive(Debug, Clone, Copy)] +struct Allocation { + start: usize, + len: usize, +} +#[derive(Debug, Clone)] +struct ChunkAlloc { + used: Vec, + chunk: usize, +} +impl ChunkAlloc { + fn new(layout: AllocLayout, chunk: usize) -> Result { + let mut used = Vec::new(); + let count = layout.bitmap_count * 64; + used.try_reserve_exact(count) + .map_err(|_| Error::OutOfMemory)?; + used.resize(count, false); + Ok(Self { used, chunk }) + } + fn alloc(&mut self, bytes: usize) -> Option { + let n = bytes.div_ceil(self.chunk); + if n == 0 { + return Some(Allocation { start: 0, len: 0 }); + } + let mut run = 0; + for i in 0..self.used.len() { + if self.used[i] { + run = 0; + } else { + run += 1; + } + if run == n { + let start = i + 1 - n; + self.used[start..=i].fill(true); + return Some(Allocation { start, len: n }); + } + } + None + } + fn free(&mut self, a: Allocation) { + self.used[a.start..a.start + a.len].fill(false); + } + fn reset(&mut self) { + self.used.fill(false); + } +} +/// Page-owned rows, cells, and bounded auxiliary storage. +#[derive(Debug, Clone)] +pub struct Page { + /// Logical dimensions; no greater than capacity. + pub size: Size, + /// Fixed logical allocation limits. + pub capacity: Capacity, + /// A full-page operation dirtied every row. + pub dirty: bool, + /// Row metadata in screen order. + pub rows: Vec, + /// Cells in row-major capacity order. + pub cells: Vec, + /// Shared nondefault cell styles. + pub styles: style::Set, + /// Shared hyperlink values. + pub hyperlink_set: hyperlink::Set, + grapheme_map: HashMap>, + grapheme_allocations: HashMap, + grapheme_alloc: ChunkAlloc, + hyperlink_map: hyperlink::Map, + string_alloc: ChunkAlloc, + link_allocations: HashMap>, + pause_integrity_checks: usize, +} +/// A cell with its page-independent managed values, used when copying pages. +#[derive(Debug, Clone)] +pub(crate) struct OwnedCell { + pub cell: Cell, + pub style: Option