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, ptr::NonNull};
7
8use crate::{
9    error::{Error, Result, from_optional_result_uninit, from_result, from_result_with_len},
10    ffi,
11    style::{self, PaletteIndex, RgbColor, Style},
12    terminal::{Point, PointCoordinate, PointSpace, Terminal},
13};
14
15/// Terminal screen identifier.
16///
17/// Identifies which screen buffer is active in the terminal.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19#[repr(u32)]
20pub enum Screen {
21    /// The primary (normal) screen.
22    #[default]
23    Primary = ffi::TerminalScreen::PRIMARY,
24    /// The alternate screen.
25    Alternate = ffi::TerminalScreen::ALTERNATE,
26}
27
28/// Resolved reference to a terminal cell position.
29///
30/// A grid reference is a resolved reference to a specific cell position in
31/// the terminal's internal page structure. Obtain a grid reference from
32/// [`Terminal::grid_ref`][crate::Terminal::grid_ref], then extract the cell
33/// or row via [`GridRef::cell`] and [`GridRef::row`].
34///
35/// A grid reference is only valid until the next update to the terminal
36/// instance. There is no guarantee that a grid reference will remain valid
37/// after ANY operation, even if a seemingly unrelated part of the grid is
38/// changed, so any information related to the grid reference should be read
39/// and cached immediately after obtaining the grid reference.
40///
41/// This API is not meant to be used as the core of render loop.
42/// It isn't built to sustain the framerates needed for rendering large screens.
43/// Use the render state API for that.
44#[derive(Clone, Debug)]
45pub struct GridRef<'t> {
46    pub(crate) inner: ffi::GridRef,
47    pub(crate) _phan: PhantomData<&'t ffi::Terminal>,
48}
49
50impl GridRef<'_> {
51    pub(crate) unsafe fn from_raw(inner: ffi::GridRef) -> Self {
52        Self {
53            inner,
54            _phan: PhantomData,
55        }
56    }
57
58    /// Get the row from a grid reference.
59    pub fn row(&self) -> Result<Row> {
60        let mut v = ffi::Row::default();
61        let result =
62            unsafe { ffi::ghostty_grid_ref_row(std::ptr::from_ref(&self.inner), &raw mut v) };
63        from_result(result)?;
64        Ok(Row(v))
65    }
66    /// Get the cell from a grid reference.
67    pub fn cell(&self) -> Result<Cell> {
68        let mut v = ffi::Cell::default();
69        let result =
70            unsafe { ffi::ghostty_grid_ref_cell(std::ptr::from_ref(&self.inner), &raw mut v) };
71        from_result(result)?;
72        Ok(Cell(v))
73    }
74    /// Get the style of the cell at the grid reference's position.
75    pub fn style(&self) -> Result<Style> {
76        let mut v = ffi::Style::default();
77        let result =
78            unsafe { ffi::ghostty_grid_ref_style(std::ptr::from_ref(&self.inner), &raw mut v) };
79        from_result(result)?;
80        Style::try_from(v)
81    }
82
83    /// Get the grapheme cluster codepoints for the cell at the grid
84    /// reference's position.
85    ///
86    /// Writes the full grapheme cluster (the cell's primary codepoint
87    /// followed by any combining codepoints) into the provided buffer.
88    /// If the cell has no text, `Ok(0)` is returned.
89    ///
90    /// If the buffer is too small, the function returns
91    /// `Err(Error::OutOfSpace { required })` where `required` is the
92    /// required number of codepoints. The caller can then retry with
93    /// a sufficiently sized buffer.
94    pub fn graphemes(&self, buf: &mut [char]) -> Result<usize> {
95        let mut len = 0;
96        let result = unsafe {
97            ffi::ghostty_grid_ref_graphemes(
98                std::ptr::from_ref(&self.inner),
99                std::ptr::from_mut(buf).cast(),
100                buf.len(),
101                &raw mut len,
102            )
103        };
104        from_result_with_len(result, len)
105    }
106
107    /// Get the hyperlink URI for the cell at the grid reference's position.
108    ///
109    /// Writes the URI bytes into the provided buffer.
110    /// If the cell has no hyperlink, `Ok(0)` is returned.
111    ///
112    /// If the buffer is too small, the function returns
113    /// `Err(Error::OutOfSpace { required })` where `required` is the
114    /// required number of codepoints. The caller can then retry with
115    /// a sufficiently sized buffer.
116    pub fn hyperlink_uri(&self, buf: &mut [u8]) -> Result<usize> {
117        let mut len = 0;
118        let result = unsafe {
119            ffi::ghostty_grid_ref_hyperlink_uri(
120                std::ptr::from_ref(&self.inner),
121                std::ptr::from_mut(buf).cast(),
122                buf.len(),
123                &raw mut len,
124            )
125        };
126        from_result_with_len(result, len)
127    }
128}
129
130/// Owned grid references that move with the terminal.
131///
132/// A tracked grid reference follows its cell across normal screen operations.
133/// For example scrolling, scrollback pruning, resize/reflow, and other
134/// terminal mutations update the tracked reference automatically.
135///
136/// A tracked reference can still lose its original semantic location.
137/// This can happen when the underlying grid is reset, pruned, or otherwise
138/// discarded in a way that cannot be mapped to a meaningful new cell.
139/// In that state, [`TrackedGridRef::has_value`] returns `false` and
140/// [`TrackedGridRef::snapshot`] / [`TrackedGridRef::point`] return `Ok(None)`.
141/// The handle remains valid, and callers may move it to a new point with
142/// [`TrackedGridRef::set`].
143///
144/// To read cell data from a tracked reference, first snapshot it with
145/// [`TrackedGridRef::snapshot`]. The returned [`GridRef`] is again an
146/// untracked reference and follows the same short lifetime rules as any
147/// other untracked grid reference.
148///
149/// A tracked reference belongs to the terminal screen/page-list that was
150/// active when it was created or last set. Converting it to a point uses that
151/// owning screen/page-list, even if the terminal has since switched between
152/// primary and alternate screens. Calling [`TrackedGridRef::set`] resolves
153/// the new point against the terminal's currently active screen/page-list
154/// and may move the tracked reference between screens.
155///
156/// If the tracked grid reference outlives the terminal it is created from,
157/// it remains valid, but all APIs return either `false` or `Ok(None)`.
158///
159/// Each tracked reference adds bookkeeping to terminal mutations. Use them
160/// sparingly for long-lived anchors such as selections, search state, marks,
161/// or application-side bookmarks.
162#[derive(Debug)]
163pub struct TrackedGridRef {
164    inner: NonNull<ffi::TrackedGridRefImpl>,
165    terminal: NonNull<ffi::TerminalImpl>,
166}
167
168impl TrackedGridRef {
169    pub(crate) fn new(
170        inner: NonNull<ffi::TrackedGridRefImpl>,
171        terminal: NonNull<ffi::TerminalImpl>,
172    ) -> Self {
173        Self { inner, terminal }
174    }
175
176    /// Whether a tracked grid reference currently has a meaningful value.
177    ///
178    /// If the terminal that created the tracked reference has been dropped,
179    /// this returns false.
180    pub fn has_value(&self) -> bool {
181        unsafe { ffi::ghostty_tracked_grid_ref_has_value(self.inner.as_ptr()) }
182    }
183
184    /// Snapshot a tracked grid reference into a regular [`GridRef`].
185    ///
186    /// The returned [`GridRef`] is an untracked snapshot and has the same lifetime
187    /// rules as [`Terminal::grid_ref`]: it is only valid until the next terminal update.
188    /// Snapshot immediately before calling [`GridRef::cell`], [`GridRef::row`],
189    /// [`GridRef::graphemes`], [`GridRef::hyperlink_uri`], or [`GridRef::style`],
190    ///
191    /// If the tracked reference no longer has a meaningful value, this returns
192    /// `Ok(None)`. This includes references whose owning terminal has been dropped.
193    pub fn snapshot<'t>(&self, terminal: &'t Terminal<'_, '_>) -> Result<Option<GridRef<'t>>> {
194        // The C ghostty_tracked_grid_ref_snapshot does not take a terminal, so
195        // we validate the pairing here to keep the returned GridRef's lifetime
196        // soundly tied to a terminal that actually owns the underlying pin.
197        if self.terminal != terminal.inner.ptr {
198            return Err(Error::InvalidValue);
199        }
200        let mut grid_ref = MaybeUninit::new(ffi::sized!(ffi::GridRef));
201        let result = unsafe {
202            ffi::ghostty_tracked_grid_ref_snapshot(self.inner.as_ptr(), grid_ref.as_mut_ptr())
203        };
204
205        from_optional_result_uninit(result, grid_ref).map(|value| {
206            value.map(|raw| unsafe {
207                // SAFETY: A successful libghostty snapshot initializes a
208                // short-lived untracked grid reference for the provided
209                // terminal. The returned Rust lifetime is tied to that
210                // terminal borrow.
211                GridRef::from_raw(raw)
212            })
213        })
214    }
215
216    /// Convert a tracked grid reference to a point in the requested coordinate space.
217    ///
218    /// This is the tracked equivalent of [`Terminal::point_from_grid_ref`].
219    /// Unlike snapshotting, this does not expose an intermediate untracked
220    /// [`GridRef`].
221    ///
222    /// A tracked reference is resolved against the terminal screen/page-list
223    /// that currently owns the reference. If the terminal has switched between
224    /// primary and alternate screens since the reference was created or last
225    /// set, this may be different from the terminal's currently active screen.
226    ///
227    /// If the tracked reference no longer has a meaningful value, this returns
228    /// `Ok(None)`. `Ok(None` is also returned when the reference cannot be represented
229    /// in the requested coordinate space, including after the terminal that
230    /// created the tracked reference has been dropped.
231    pub fn point(&self, space: PointSpace) -> Result<Option<PointCoordinate>> {
232        let mut point = MaybeUninit::<ffi::PointCoordinate>::zeroed();
233        let result = unsafe {
234            ffi::ghostty_tracked_grid_ref_point(
235                self.inner.as_ptr(),
236                space.into_raw(),
237                point.as_mut_ptr(),
238            )
239        };
240
241        from_optional_result_uninit(result, point).map(|value| value.map(Into::into))
242    }
243
244    /// Move an existing tracked grid reference to a new terminal point.
245    ///
246    /// On success, the tracked reference begins tracking the new point and any
247    /// prior "no value" state is cleared. On `Err(Error::OutOfMemory)`, the original
248    /// tracked reference is left unchanged.
249    ///
250    /// The terminal must be the same terminal that created the tracked reference.
251    /// The point is resolved against the terminal screen/page-list that is active
252    /// at the time this function is called. If the terminal has switched between
253    /// primary and alternate screens, this may move the tracked reference from
254    /// one screen/page-list to the other.
255    pub fn set(&mut self, terminal: &mut Terminal<'_, '_>, point: Point) -> Result<&mut Self> {
256        // The C layer validates the terminal/tracked-ref pairing and returns
257        // GHOSTTY_INVALID_VALUE on mismatch, so we don't duplicate the check
258        // on the Rust side.
259        let result = unsafe {
260            ffi::ghostty_tracked_grid_ref_set(
261                self.inner.as_ptr(),
262                terminal.inner.as_raw(),
263                point.into(),
264            )
265        };
266        from_result(result)?;
267        Ok(self)
268    }
269}
270
271impl Drop for TrackedGridRef {
272    fn drop(&mut self) {
273        unsafe { ffi::ghostty_tracked_grid_ref_free(self.inner.as_ptr()) }
274    }
275}
276
277/// Represents a single terminal row.
278///
279/// The internal layout is opaque and must be queried via its methods.
280/// Obtain cell values from terminal query APIs.
281#[derive(Clone, Copy, Debug, PartialEq, Eq)]
282pub struct Row(pub(crate) ffi::Row);
283
284impl Row {
285    fn get<T>(&self, tag: ffi::RowData::Type) -> Result<T> {
286        let mut value = MaybeUninit::<T>::zeroed();
287        let result = unsafe { ffi::ghostty_row_get(self.0, tag, value.as_mut_ptr().cast()) };
288        // Since we manually model every possible query, this should never fail.
289        from_result(result)?;
290        // SAFETY: Value should be initialized after successful call.
291        Ok(unsafe { value.assume_init() })
292    }
293
294    /// Whether this row is soft-wrapped.
295    pub fn is_wrapped(self) -> Result<bool> {
296        self.get(ffi::RowData::WRAP)
297    }
298    /// Whether this row is a continuation of a soft-wrapped row.
299    pub fn is_wrap_continuation(self) -> Result<bool> {
300        self.get(ffi::RowData::WRAP_CONTINUATION)
301    }
302    /// Whether any cells in this row have grapheme clusters.
303    pub fn has_grapheme_cluster(self) -> Result<bool> {
304        self.get(ffi::RowData::GRAPHEME)
305    }
306    /// Whether any cells in this row have styling (may have false positives).
307    pub fn is_styled(self) -> Result<bool> {
308        self.get(ffi::RowData::STYLED)
309    }
310    /// Whether any cells in this row have hyperlinks (may have false positives).
311    pub fn has_hyperlink(self) -> Result<bool> {
312        self.get(ffi::RowData::HYPERLINK)
313    }
314    /// The semantic prompt state of this row.
315    pub fn semantic_prompt(self) -> Result<RowSemanticPrompt> {
316        self.get::<ffi::RowSemanticPrompt::Type>(ffi::RowData::SEMANTIC_PROMPT)
317            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
318    }
319    /// Whether this row contains a Kitty virtual placeholder.
320    pub fn has_kitty_virtual_placeholder(self) -> Result<bool> {
321        self.get(ffi::RowData::KITTY_VIRTUAL_PLACEHOLDER)
322    }
323    /// Whether this row is dirty and requires a redraw.
324    pub fn is_dirty(self) -> Result<bool> {
325        self.get(ffi::RowData::DIRTY)
326    }
327}
328
329/// Represents a single terminal cell.
330///
331/// The internal layout is opaque and must be queried via its methods.
332/// Obtain cell values from terminal query APIs.
333#[derive(Clone, Copy, Debug, PartialEq, Eq)]
334pub struct Cell(pub(crate) ffi::Cell);
335
336impl Cell {
337    fn get<T>(&self, tag: ffi::CellData::Type) -> Result<T> {
338        let mut value = MaybeUninit::<T>::zeroed();
339        let result = unsafe { ffi::ghostty_cell_get(self.0, tag, value.as_mut_ptr().cast()) };
340        // Since we manually model every possible query, this should never fail.
341        from_result(result)?;
342        // SAFETY: Value should be initialized after successful call.
343        Ok(unsafe { value.assume_init() })
344    }
345
346    /// The codepoint of the cell (0 if empty or bg-color-only).
347    pub fn codepoint(self) -> Result<u32> {
348        self.get(ffi::CellData::CODEPOINT)
349    }
350    /// The content tag describing what kind of content is in the cell.
351    pub fn content_tag(self) -> Result<CellContentTag> {
352        self.get::<ffi::CellContentTag::Type>(ffi::CellData::CONTENT_TAG)
353            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
354    }
355    /// The wide property of the cell.
356    pub fn wide(self) -> Result<CellWide> {
357        self.get::<ffi::CellWide::Type>(ffi::CellData::WIDE)
358            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
359    }
360    /// Whether the cell has text to render.
361    pub fn has_text(self) -> Result<bool> {
362        self.get(ffi::CellData::HAS_TEXT)
363    }
364    /// Whether the cell has non-default styling.
365    pub fn has_styling(self) -> Result<bool> {
366        self.get(ffi::CellData::HAS_STYLING)
367    }
368    /// The style ID for the cell (for use with style lookups).
369    pub fn style_id(self) -> Result<style::Id> {
370        self.get(ffi::CellData::STYLE_ID).map(style::Id)
371    }
372    /// Whether the cell has a hyperlink.
373    pub fn has_hyperlink(self) -> Result<bool> {
374        self.get(ffi::CellData::HAS_HYPERLINK)
375    }
376    /// Whether the cell is protected.
377    pub fn is_protected(self) -> Result<bool> {
378        self.get(ffi::CellData::PROTECTED)
379    }
380    /// The semantic content type of the cell (from OSC 133).
381    pub fn semantic_content(self) -> Result<CellSemanticContent> {
382        self.get::<ffi::CellSemanticContent::Type>(ffi::CellData::SEMANTIC_CONTENT)
383            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
384    }
385
386    /// The palette index for the cell's background color.
387    ///
388    /// Only valid when [`Cell::content_tag`] is [`CellContentTag::BgColorPalette`].
389    pub fn bg_color_palette(self) -> Result<PaletteIndex> {
390        self.get(ffi::CellData::COLOR_PALETTE).map(PaletteIndex)
391    }
392    /// The RGB color value for the cell's background color.
393    ///
394    /// Only valid when [`Cell::content_tag`] is [`CellContentTag::BgColorRgb`].
395    pub fn bg_color_rgb(self) -> Result<RgbColor> {
396        Ok(self.get::<ffi::ColorRgb>(ffi::CellData::COLOR_RGB)?.into())
397    }
398}
399
400/// Row semantic prompt state.
401///
402/// Indicates whether any cells in a row are part of a shell prompt, as reported by OSC 133 sequences.
403#[repr(u32)]
404#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
405pub enum RowSemanticPrompt {
406    /// No prompt cells in this row.
407    None = ffi::RowSemanticPrompt::NONE,
408    /// Prompt cells exist and this is a primary prompt line.
409    Prompt = ffi::RowSemanticPrompt::PROMPT,
410    /// Prompt cells exist and this is a continuation line.
411    Continuation = ffi::RowSemanticPrompt::PROMPT_CONTINUATION,
412}
413
414/// Cell content tag.
415///
416/// Describes what kind of content a cell holds.
417#[repr(u32)]
418#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
419pub enum CellContentTag {
420    /// A single codepoint (may be zero for empty).
421    Codepoint = ffi::CellContentTag::CODEPOINT,
422    /// A codepoint that is part of a multi-codepoint grapheme cluster.
423    CodepointGrapheme = ffi::CellContentTag::CODEPOINT_GRAPHEME,
424    /// No text; background color from palette.
425    BgColorPalette = ffi::CellContentTag::BG_COLOR_PALETTE,
426    /// No text; background color as RGB.
427    BgColorRgb = ffi::CellContentTag::BG_COLOR_RGB,
428}
429
430/// Cell wide property.
431///
432/// Describes the width behavior of a cell.
433#[repr(u32)]
434#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
435pub enum CellWide {
436    /// Not a wide character, cell width 1.
437    Narrow = ffi::CellWide::NARROW,
438    /// Wide character, cell width 2.  
439    Wide = ffi::CellWide::WIDE,
440    /// Spacer after wide character. Do not render.
441    SpacerTail = ffi::CellWide::SPACER_TAIL,
442    /// Spacer at end of soft-wrapped line for a wide character.
443    SpacerHead = ffi::CellWide::SPACER_HEAD,
444}
445
446/// Semantic content type of a cell.
447///
448/// Set by semantic prompt sequences (OSC 133) to distinguish between
449/// command output, user input, and shell prompt text.
450#[repr(u32)]
451#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
452pub enum CellSemanticContent {
453    /// Regular output content, such as command output.
454    Output = ffi::CellSemanticContent::OUTPUT,
455    /// Content that is part of user input.
456    Input = ffi::CellSemanticContent::INPUT,
457    /// Content that is part of a shell prompt.
458    Prompt = ffi::CellSemanticContent::PROMPT,
459}