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}