Skip to main content

libghostty_vt/
screen.rs

1//! Terminal screen cell and row types.
2//!
3//! These types represent the contents of a terminal screen.
4//! A [`Cell`] is a single grid cell and a [`Row`] is a single row.
5//! Both are opaque values whose fields are accessed via their methods.
6use std::{marker::PhantomData, mem::MaybeUninit};
7
8use crate::{
9    error::{Error, Result, from_result, from_result_with_len},
10    ffi,
11    style::{self, PaletteIndex, RgbColor, Style},
12};
13
14/// Resolved reference to a terminal cell position.
15///
16/// A grid reference is a resolved reference to a specific cell position in
17/// the terminal's internal page structure. Obtain a grid reference from
18/// [`Terminal::grid_ref`][crate::Terminal::grid_ref], then extract the cell
19/// or row via [`GridRef::cell`] and [`GridRef::row`].
20///
21/// A grid reference is only valid until the next update to the terminal
22/// instance. There is no guarantee that a grid reference will remain valid
23/// after ANY operation, even if a seemingly unrelated part of the grid is
24/// changed, so any information related to the grid reference should be read
25/// and cached immediately after obtaining the grid reference.
26///
27/// This API is not meant to be used as the core of render loop.
28/// It isn't built to sustain the framerates needed for rendering large screens.
29/// Use the render state API for that.
30#[derive(Clone, Debug)]
31pub struct GridRef<'t> {
32    pub(crate) inner: ffi::GhosttyGridRef,
33    pub(crate) _phan: PhantomData<&'t ffi::GhosttyTerminal>,
34}
35
36impl GridRef<'_> {
37    /// Get the row from a grid reference.
38    pub fn row(&self) -> Result<Row> {
39        let mut v = ffi::GhosttyRow::default();
40        let result = unsafe { ffi::ghostty_grid_ref_row(std::ptr::from_ref(&self.inner), &raw mut v) };
41        from_result(result)?;
42        Ok(Row(v))
43    }
44    /// Get the cell from a grid reference.
45    pub fn cell(&self) -> Result<Cell> {
46        let mut v = ffi::GhosttyCell::default();
47        let result = unsafe { ffi::ghostty_grid_ref_cell(std::ptr::from_ref(&self.inner), &raw mut v) };
48        from_result(result)?;
49        Ok(Cell(v))
50    }
51    /// Get the style of the cell at the grid reference's position.
52    pub fn style(&self) -> Result<Style> {
53        let mut v = ffi::GhosttyStyle::default();
54        let result =
55            unsafe { ffi::ghostty_grid_ref_style(std::ptr::from_ref(&self.inner), &raw mut v) };
56        from_result(result)?;
57        Style::try_from(v)
58    }
59
60    /// Get the grapheme cluster codepoints for the cell at the grid
61    /// reference's position.
62    ///
63    /// Writes the full grapheme cluster (the cell's primary codepoint
64    /// followed by any combining codepoints) into the provided buffer.
65    /// If the cell has no text, `Ok(0)` is returned.
66    ///
67    /// If the buffer is too small, the function returns
68    /// `Err(Error::OutOfSpace { required })` where `required` is the
69    /// required number of codepoints. The caller can then retry with
70    /// a sufficiently sized buffer.
71    pub fn graphemes(&self, buf: &mut [char]) -> Result<usize> {
72        let mut len = 0;
73        let result = unsafe {
74            ffi::ghostty_grid_ref_graphemes(
75                std::ptr::from_ref(&self.inner),
76                std::ptr::from_mut(buf).cast(),
77                buf.len(),
78                &raw mut len,
79            )
80        };
81        from_result_with_len(result, len)
82    }
83}
84
85/// Represents a single terminal row.
86///
87/// The internal layout is opaque and must be queried via its methods.
88/// Obtain cell values from terminal query APIs.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub struct Row(pub(crate) ffi::GhosttyRow);
91
92impl Row {
93    fn get<T>(&self, tag: ffi::GhosttyRowData) -> Result<T> {
94        let mut value = MaybeUninit::<T>::zeroed();
95        let result = unsafe { ffi::ghostty_row_get(self.0, tag, value.as_mut_ptr().cast()) };
96        // Since we manually model every possible query, this should never fail.
97        from_result(result)?;
98        // SAFETY: Value should be initialized after successful call.
99        Ok(unsafe { value.assume_init() })
100    }
101
102    /// Whether this row is soft-wrapped.
103    pub fn is_wrapped(self) -> Result<bool> {
104        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP)
105    }
106    /// Whether this row is a continuation of a soft-wrapped row.
107    pub fn is_wrap_continuation(self) -> Result<bool> {
108        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP_CONTINUATION)
109    }
110    /// Whether any cells in this row have grapheme clusters.
111    pub fn has_grapheme_cluster(self) -> Result<bool> {
112        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_GRAPHEME)
113    }
114    /// Whether any cells in this row have styling (may have false positives).
115    pub fn is_styled(self) -> Result<bool> {
116        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_STYLED)
117    }
118    /// Whether any cells in this row have hyperlinks (may have false positives).
119    pub fn has_hyperlink(self) -> Result<bool> {
120        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_HYPERLINK)
121    }
122    /// The semantic prompt state of this row.
123    pub fn semantic_prompt(self) -> Result<RowSemanticPrompt> {
124        self.get::<ffi::GhosttyRowSemanticPrompt>(
125            ffi::GhosttyRowData_GHOSTTY_ROW_DATA_SEMANTIC_PROMPT,
126        )
127        .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
128    }
129    /// Whether this row contains a Kitty virtual placeholder.
130    pub fn has_kitty_virtual_placeholder(self) -> Result<bool> {
131        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER)
132    }
133    /// Whether this row is dirty and requires a redraw.
134    pub fn is_dirty(self) -> Result<bool> {
135        self.get(ffi::GhosttyRowData_GHOSTTY_ROW_DATA_DIRTY)
136    }
137}
138
139/// Represents a single terminal cell.
140///
141/// The internal layout is opaque and must be queried via its methods.
142/// Obtain cell values from terminal query APIs.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct Cell(pub(crate) ffi::GhosttyCell);
145
146impl Cell {
147    fn get<T>(&self, tag: ffi::GhosttyCellData) -> Result<T> {
148        let mut value = MaybeUninit::<T>::zeroed();
149        let result = unsafe { ffi::ghostty_cell_get(self.0, tag, value.as_mut_ptr().cast()) };
150        // Since we manually model every possible query, this should never fail.
151        from_result(result)?;
152        // SAFETY: Value should be initialized after successful call.
153        Ok(unsafe { value.assume_init() })
154    }
155
156    /// The codepoint of the cell (0 if empty or bg-color-only).
157    pub fn codepoint(self) -> Result<u32> {
158        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CODEPOINT)
159    }
160    /// The content tag describing what kind of content is in the cell.
161    pub fn content_tag(self) -> Result<CellContentTag> {
162        self.get::<ffi::GhosttyCellContentTag>(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CONTENT_TAG)
163            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
164    }
165    /// The wide property of the cell.
166    pub fn wide(self) -> Result<CellWide> {
167        self.get::<ffi::GhosttyCellWide>(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE)
168            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
169    }
170    /// Whether the cell has text to render.
171    pub fn has_text(self) -> Result<bool> {
172        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_TEXT)
173    }
174    /// Whether the cell has non-default styling.
175    pub fn has_styling(self) -> Result<bool> {
176        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_STYLING)
177    }
178    /// The style ID for the cell (for use with style lookups).
179    pub fn style_id(self) -> Result<style::Id> {
180        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_STYLE_ID)
181            .map(style::Id)
182    }
183    /// Whether the cell has a hyperlink.
184    pub fn has_hyperlink(self) -> Result<bool> {
185        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_HYPERLINK)
186    }
187    /// Whether the cell is protected.
188    pub fn is_protected(self) -> Result<bool> {
189        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_PROTECTED)
190    }
191    /// The semantic content type of the cell (from OSC 133).
192    pub fn semantic_content(self) -> Result<CellSemanticContent> {
193        self.get::<ffi::GhosttyCellSemanticContent>(
194            ffi::GhosttyCellData_GHOSTTY_CELL_DATA_SEMANTIC_CONTENT,
195        )
196        .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
197    }
198
199    /// The palette index for the cell's background color.
200    ///
201    /// Only valid when [`Cell::content_tag`] is [`CellContentTag::BgColorPalette`].
202    pub fn bg_color_palette(self) -> Result<PaletteIndex> {
203        self.get(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_PALETTE)
204            .map(PaletteIndex)
205    }
206    /// The RGB color value for the cell's background color.
207    ///
208    /// Only valid when [`Cell::content_tag`] is [`CellContentTag::BgColorRgb`].
209    pub fn bg_color_rgb(self) -> Result<RgbColor> {
210        Ok(self
211            .get::<ffi::GhosttyColorRgb>(ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_RGB)?
212            .into())
213    }
214}
215
216/// Row semantic prompt state.
217///
218/// Indicates whether any cells in a row are part of a shell prompt, as reported by OSC 133 sequences.
219#[repr(u32)]
220#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
221pub enum RowSemanticPrompt {
222    /// No prompt cells in this row.
223    None = ffi::GhosttyRowSemanticPrompt_GHOSTTY_ROW_SEMANTIC_NONE,
224    /// Prompt cells exist and this is a primary prompt line.
225    Prompt = ffi::GhosttyRowSemanticPrompt_GHOSTTY_ROW_SEMANTIC_PROMPT,
226    /// Prompt cells exist and this is a continuation line.
227    Continuation = ffi::GhosttyRowSemanticPrompt_GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION,
228}
229
230/// Cell content tag.
231///
232/// Describes what kind of content a cell holds.
233#[repr(u32)]
234#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
235pub enum CellContentTag {
236    /// A single codepoint (may be zero for empty).
237    Codepoint = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_CODEPOINT,
238    /// A codepoint that is part of a multi-codepoint grapheme cluster.
239    CodepointGrapheme = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME,
240    /// No text; background color from palette.
241    BgColorPalette = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE,
242    /// No text; background color as RGB.
243    BgColorRgb = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_RGB,
244}
245
246/// Cell wide property.
247///
248/// Describes the width behavior of a cell.
249#[repr(u32)]
250#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
251pub enum CellWide {
252    /// Not a wide character, cell width 1.
253    Narrow = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW,
254    /// Wide character, cell width 2.  
255    Wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_WIDE,
256    /// Spacer after wide character. Do not render.
257    SpacerTail = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_TAIL,
258    /// Spacer at end of soft-wrapped line for a wide character.
259    SpacerHead = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_HEAD,
260}
261
262/// Semantic content type of a cell.
263///
264/// Set by semantic prompt sequences (OSC 133) to distinguish between
265/// command output, user input, and shell prompt text.
266#[repr(u32)]
267#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
268pub enum CellSemanticContent {
269    /// Regular output content, such as command output.
270    Output = ffi::GhosttyCellSemanticContent_GHOSTTY_CELL_SEMANTIC_OUTPUT,
271    /// Content that is part of user input.
272    Input = ffi::GhosttyCellSemanticContent_GHOSTTY_CELL_SEMANTIC_INPUT,
273    /// Content that is part of a shell prompt.
274    Prompt = ffi::GhosttyCellSemanticContent_GHOSTTY_CELL_SEMANTIC_PROMPT,
275}