Skip to main content

libghostty_vt/
render.rs

1//! Managing [render states](RenderState) of the terminal.
2
3use std::{convert::Into, marker::PhantomData, mem::MaybeUninit};
4
5use crate::{
6    alloc::{Allocator, Object},
7    error::{Error, Result, from_optional_result, from_result},
8    ffi,
9    screen::{Cell, Row},
10    style::{RgbColor, Style},
11    terminal::Terminal,
12};
13
14pub use ffi::RenderStateRowSelection as RowSelection;
15
16/// Represents the state required to render a visible screen (a viewport) of
17/// a terminal instance.
18///
19/// This is stateful and optimized for repeated updates from a single terminal
20/// instance and only updating dirty regions of the screen.
21///
22/// The key design principle of this API is that it only needs read/write
23/// access to the terminal instance during the update call. This allows the
24/// render state to minimally impact terminal IO performance and also allows
25/// the renderer to be safely multi-threaded (as long as a lock is held
26/// during the update call to ensure exclusive access to the terminal instance).
27///
28/// The basic usage of this API is:
29///
30///  1. Create an empty render state
31///  2. Update it from a terminal instance whenever you need.
32///  3. Read from the render state to get the data needed to draw your frame.
33///
34/// # Dirty Tracking
35///
36/// Dirty tracking is a key feature of the render state that allows renderers
37/// to efficiently determine what parts of the screen have changed and only
38/// redraw changed regions.
39///
40/// The render state API keeps track of dirty state at two independent layers:
41/// a global dirty state that indicates whether the entire frame is clean,
42/// partially dirty, or fully dirty, and a per-row dirty state that allows
43/// tracking which rows in a partially dirty frame have changed.
44///
45/// The user of the render state API is expected to unset both of these.
46/// The update call does not unset dirty state, it only updates it.
47///
48/// An extremely important detail: **setting one dirty state doesn't unset
49/// the other.** For example, setting the global dirty state to false does
50/// not reset the row-level dirty flags. So, the caller of the render state
51/// API must be careful to manage both layers of dirty state correctly.
52///
53/// # Examples
54///
55/// ## Creating and updating render state
56///
57/// ```rust
58/// // Create a terminal and render state, then update the render state
59/// // from the terminal. The render state captures a snapshot of everything
60/// // needed to draw a frame.
61/// use libghostty_vt::{Terminal, TerminalOptions, RenderState};
62///
63/// let mut terminal = Terminal::new(TerminalOptions {
64///     cols: 40,
65///     rows: 5,
66///     max_scrollback: 10000,
67/// }).unwrap();
68///
69/// let mut render_state = RenderState::new().unwrap();
70///
71/// // Feed some styled content into the terminal.
72/// terminal.vt_write(b"Hello, \x1b[1;32mworld\x1b[0m!\r\n");
73/// terminal.vt_write(b"\x1b[4munderlined\x1b[0m text\r\n");
74/// terminal.vt_write(b"\x1b[38;2;255;128;0morange\x1b[0m\r\n");
75///
76/// assert!(render_state.update(&terminal).is_ok());
77/// ```
78///
79/// ## Checking dirty state
80///
81/// ```rust
82/// // Check the global dirty state to decide how much work the renderer
83/// // needs to do. After rendering, reset it to false.
84/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState, render::Dirty};
85/// # let terminal = Terminal::new(TerminalOptions {
86/// #     cols: 80,
87/// #     rows: 25,
88/// #     max_scrollback: 10000,
89/// # }).unwrap();
90/// # let mut render_state = RenderState::new().unwrap();
91/// let snapshot = render_state.update(&terminal).unwrap();
92///
93/// match snapshot.dirty().unwrap() {
94///     Dirty::Clean => println!("Frame is clean, nothing to draw."),
95///     Dirty::Partial => println!("Partial redraw needed."),
96///     Dirty::Full => println!("Full redraw needed."),
97/// }
98/// ```
99///
100/// ## Reading colors
101///
102/// ```rust
103/// // Retrieve colors (background, foreground, palette) from the render
104/// // state. These are needed to resolve palette-indexed cell colors.
105/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState};
106/// # let terminal = Terminal::new(TerminalOptions {
107/// #     cols: 80,
108/// #     rows: 25,
109/// #     max_scrollback: 10000,
110/// # }).unwrap();
111/// # let mut render_state = RenderState::new().unwrap();
112/// let snapshot = render_state.update(&terminal).unwrap();
113/// let colors = snapshot.colors().unwrap();
114///
115/// println!(
116///     "Background: {:02x}{:02x}{:02x}",
117///     colors.background.r, colors.background.g, colors.background.b
118/// );
119/// println!(
120///     "Foreground: {:02x}{:02x}{:02x}",
121///     colors.background.r, colors.background.g, colors.background.b
122/// );
123/// ```
124///
125/// ## Reading cursor state
126///
127/// ```rust
128/// // Read cursor position and visual style from the render state.
129/// use libghostty_vt::render::CursorViewport;
130/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState};
131/// # let terminal = Terminal::new(TerminalOptions {
132/// #     cols: 80,
133/// #     rows: 25,
134/// #     max_scrollback: 10000,
135/// # }).unwrap();
136/// # let mut render_state = RenderState::new().unwrap();
137/// let snapshot = render_state.update(&terminal).unwrap();
138///
139/// if snapshot.cursor_visible().unwrap() {
140///     if let Some(CursorViewport { x, y, .. }) = snapshot.cursor_viewport().unwrap() {
141///         let style = snapshot.cursor_visual_style().unwrap();
142///         println!("Cursor at ({x}, {y}), style {style:?}");
143///     }
144/// }
145/// ```
146///
147/// ## Iterating rows and cells
148///
149/// ```rust
150/// // Iterate rows via the row iterator. For each dirty row, iterate its
151/// // cells, read codepoints/graphemes and styles, and emit ANSI-colored
152/// // output as a simple "renderer".
153/// use libghostty_vt::{Terminal, TerminalOptions, RenderState};
154/// use libghostty_vt::style::Underline;
155///
156/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
157/// # let terminal = Terminal::new(TerminalOptions {
158/// #     cols: 80,
159/// #     rows: 25,
160/// #     max_scrollback: 10000,
161/// # }).unwrap();
162/// # let mut render_state = RenderState::new()?;
163/// use libghostty_vt::render::{RowIterator, CellIterator};
164///
165/// // During setup:
166/// let mut rows = RowIterator::new()?;
167/// let mut cells = CellIterator::new()?;
168///
169/// // On each frame:
170/// let snapshot = render_state.update(&terminal)?;
171/// let colors = snapshot.colors()?;
172///
173/// let mut row_iter = rows.update(&snapshot)?;
174/// let mut row_index = 0;
175///
176/// while let Some(row) = row_iter.next() {
177///     // Check per-row dirty state; a real renderer would skip clean rows.
178///     print!(
179///         "Row {row_index} [{}]",
180///         if row.dirty()? { "dirty" } else { "clean" }
181///     );
182///
183///     // Get cells for this row (reuses the same cells handle).
184///     let mut cell_iter = cells.update(&row)?;
185///     while let Some(cell) = cell_iter.next() {
186///         let graphemes = cell.graphemes()?;
187///
188///         if graphemes.is_empty() {
189///             print!(" ");
190///             continue;
191///         }
192///
193///         // Resolve foreground color for this cell.
194///         let fg = cell.fg_color()?.unwrap_or(colors.foreground);
195///         // Emit ANSI true-color escape for the foreground.
196///         print!("\x1b[38;2;{};{};{}m", fg.r, fg.g, fg.b);
197///
198///         // Read the style for this cell. Returns the default style for
199///         // cells that have no explicit styling.
200///         let style = cell.style()?;
201///         if style.bold {
202///             print!("\x1b[1m");
203///         }
204///         if style.underline != Underline::None {
205///             print!("\x1b[4m");
206///         }
207///
208///         for grapheme in graphemes {
209///             print!("{}", grapheme.escape_default());
210///         }
211///         print!("\x1b[0m"); // Reset style after each cell.
212///     }
213///     println!();
214///
215///     // Clear per-row dirty flag after "rendering" it.
216///     row.set_dirty(false);
217///
218///     row_index += 1;
219/// }
220/// # Ok(())}
221/// ```
222#[derive(Debug)]
223pub struct RenderState<'alloc>(Object<'alloc, ffi::RenderStateImpl>);
224
225/// A snapshot of the render state after an update.
226///
227/// This struct exists to guard data accessed from the render state from
228/// being accidentally modified after an update. If you find yourself unable
229/// to update the render state due to borrow checker errors, make sure to
230/// drop the active snapshot (and data that depends on it) before updating.
231#[derive(Debug)]
232pub struct Snapshot<'alloc, 's>(&'s mut RenderState<'alloc>);
233
234/// Opaque handle to a render-state row iterator.
235///
236/// The row iterator must be [updated](RowIterator::update) from a snapshot of
237/// the render state in order to function, as most data is only accessible
238/// per [iteration](RowIteration).
239#[derive(Debug)]
240pub struct RowIterator<'alloc>(Object<'alloc, ffi::RenderStateRowIteratorImpl>);
241
242/// An active iteration over the rows in the render state.
243///
244/// Row iterations are created by [updating](RowIterator::update) row iterators
245/// with a snapshot of the render state. The borrow checker statically
246/// guarantees that all accesses of the data do not outlive the given snapshot,
247/// at the cost of added lifetime annotations.
248#[derive(Debug)]
249pub struct RowIteration<'alloc, 's> {
250    iter: &'s mut RowIterator<'alloc>,
251    // NOTE: While in theory the snapshot borrow should have its own
252    // lifetime 'ss where 'rs: 'ss, but it gets very unwieldy and honestly
253    // one wouldn't run into too many situations where this simpler constraint
254    // isn't enough.
255    _phan: PhantomData<&'s Snapshot<'alloc, 's>>,
256}
257
258/// Opaque handle to a render state cell iterator.
259///
260/// The cell iterator must be [updated](CellIterator::update) from a
261/// [row](RowIteration) in order to function, as most data is only
262/// accessible per [iteration](CellIteration).
263#[derive(Debug)]
264pub struct CellIterator<'alloc>(Object<'alloc, ffi::RenderStateRowCellsImpl>);
265
266/// An active iteration over the cells on a given row
267/// within the render state.
268///
269/// Cell iterations are created by [updating](CellIterator::update) row iterators
270/// at a given [row](RowIteration). The borrow checker statically
271/// guarantees that all accesses of the data do not outlive the given snapshot,
272/// at the cost of added lifetime annotations.
273#[derive(Debug)]
274pub struct CellIteration<'alloc, 's> {
275    iter: &'s mut CellIterator<'alloc>,
276    _phan: PhantomData<&'s RowIteration<'alloc, 's>>,
277}
278
279//--------------------------
280// Impl blocks
281//--------------------------
282
283impl<'alloc> RenderState<'alloc> {
284    /// Create a new render state instance.
285    pub fn new() -> Result<Self> {
286        // SAFETY: A NULL allocator is always valid
287        unsafe { Self::new_inner(std::ptr::null()) }
288    }
289
290    /// Create a new render state instance with a custom allocator.
291    ///
292    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
293    /// regarding custom memory management and lifetimes.
294    pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
295        // SAFETY: Borrow checking should forbid invalid allocators
296        unsafe { Self::new_inner(alloc.to_raw()) }
297    }
298
299    unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
300        let mut raw: ffi::RenderState = std::ptr::null_mut();
301        let result = unsafe { ffi::ghostty_render_state_new(alloc, &raw mut raw) };
302        from_result(result)?;
303        Ok(Self(Object::new(raw)?))
304    }
305
306    /// Update a render state instance from a terminal,
307    /// returning a new [snapshot](Snapshot).
308    ///
309    /// This consumes terminal/screen dirty state in the same way as the
310    /// internal render state update path.
311    ///
312    /// # Errors
313    ///
314    /// Returns `Err(Error::OutOfMemory)` if updating the state requires
315    /// allocation and that allocation fails.
316    pub fn update<'cb>(
317        &mut self,
318        terminal: &Terminal<'alloc, 'cb>,
319    ) -> Result<Snapshot<'alloc, '_>> {
320        let result =
321            unsafe { ffi::ghostty_render_state_update(self.0.as_raw(), terminal.inner.as_raw()) };
322        from_result(result)?;
323        Ok(Snapshot(self))
324    }
325}
326
327impl Drop for RenderState<'_> {
328    fn drop(&mut self) {
329        unsafe { ffi::ghostty_render_state_free(self.0.as_raw()) }
330    }
331}
332
333impl Snapshot<'_, '_> {
334    fn get<T>(&self, tag: ffi::RenderStateData::Type) -> Result<T> {
335        let mut value = MaybeUninit::<T>::zeroed();
336        let result = unsafe {
337            ffi::ghostty_render_state_get(self.0.0.as_raw(), tag, value.as_mut_ptr().cast())
338        };
339        // Since we manually model every possible query, this should never fail.
340        from_result(result)?;
341        // SAFETY: Value should be initialized after successful call.
342        Ok(unsafe { value.assume_init() })
343    }
344
345    fn set<T>(&self, tag: ffi::RenderStateOption::Type, value: &T) -> Result<()> {
346        let result = unsafe {
347            ffi::ghostty_render_state_set(self.0.0.as_raw(), tag, std::ptr::from_ref(value).cast())
348        };
349        // Since we manually model every possible query, this should never fail.
350        from_result(result)
351    }
352
353    /// Get the current dirty state.
354    pub fn dirty(&self) -> Result<Dirty> {
355        self.get::<ffi::RenderStateDirty::Type>(ffi::RenderStateData::DIRTY)
356            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
357    }
358
359    /// Get the viewport width.
360    pub fn cols(&self) -> Result<u16> {
361        self.get(ffi::RenderStateData::COLS)
362    }
363
364    /// Get the viewport height.
365    pub fn rows(&self) -> Result<u16> {
366        self.get(ffi::RenderStateData::ROWS)
367    }
368
369    /// Get the cursor color that may have been explicitly set by the terminal state.
370    pub fn cursor_color(&self) -> Result<Option<RgbColor>> {
371        let has_value = self.get(ffi::RenderStateData::COLOR_CURSOR_HAS_VALUE)?;
372        if has_value {
373            let color = self.get(ffi::RenderStateData::COLOR_CURSOR)?;
374            Ok(Some(color))
375        } else {
376            Ok(None)
377        }
378    }
379
380    /// Whether the cursor is currently visible based on terminal modes.
381    pub fn cursor_visible(&self) -> Result<bool> {
382        self.get(ffi::RenderStateData::CURSOR_VISIBLE)
383    }
384
385    /// Whether the cursor is currently blinking based on terminal modes.
386    pub fn cursor_blinking(&self) -> Result<bool> {
387        self.get(ffi::RenderStateData::CURSOR_BLINKING)
388    }
389
390    /// Whether the cursor is at a password input field.
391    pub fn cursor_password_input(&self) -> Result<bool> {
392        self.get(ffi::RenderStateData::CURSOR_PASSWORD_INPUT)
393    }
394
395    /// Get the visual style of the cursor.
396    pub fn cursor_visual_style(&self) -> Result<CursorVisualStyle> {
397        self.get::<ffi::RenderStateCursorVisualStyle::Type>(
398            ffi::RenderStateData::CURSOR_VISUAL_STYLE,
399        )
400        .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
401    }
402
403    /// Get the relative position of the cursor and other information
404    /// if it is currently visible within the viewport.
405    pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>> {
406        let has_value = self.get(ffi::RenderStateData::CURSOR_VIEWPORT_HAS_VALUE)?;
407        if has_value {
408            let x = self.get(ffi::RenderStateData::CURSOR_VIEWPORT_X)?;
409            let y = self.get(ffi::RenderStateData::CURSOR_VIEWPORT_Y)?;
410            let at_wide_tail = self.get(ffi::RenderStateData::CURSOR_VIEWPORT_WIDE_TAIL)?;
411            Ok(Some(CursorViewport { x, y, at_wide_tail }))
412        } else {
413            Ok(None)
414        }
415    }
416
417    /// Get the current color information from a render state.
418    pub fn colors(&self) -> Result<Colors> {
419        let mut colors = ffi::sized!(ffi::RenderStateColors);
420        let result =
421            unsafe { ffi::ghostty_render_state_colors_get(self.0.0.as_raw(), &raw mut colors) };
422        from_result(result)?;
423
424        Ok(Colors {
425            background: colors.background.into(),
426            foreground: colors.foreground.into(),
427            cursor: if colors.cursor_has_value {
428                Some(colors.cursor.into())
429            } else {
430                None
431            },
432            palette: colors.palette.map(Into::into),
433        })
434    }
435
436    /// Set dirty state.
437    pub fn set_dirty(&self, dirty: Dirty) -> Result<()> {
438        self.set(
439            ffi::RenderStateOption::DIRTY,
440            &(dirty as ffi::RenderStateDirty::Type),
441        )
442    }
443}
444
445impl<'alloc> RowIterator<'alloc> {
446    /// Create a new row iterator instance.
447    pub fn new() -> Result<Self> {
448        // SAFETY: A NULL allocator is always valid
449        unsafe { Self::new_inner(std::ptr::null()) }
450    }
451
452    /// Create a new cell iterator instance with a custom allocator.
453    ///
454    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
455    /// regarding custom memory management and lifetimes.
456    pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
457        // SAFETY: Borrow checking should forbid invalid allocators
458        unsafe { Self::new_inner(alloc.to_raw()) }
459    }
460
461    unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
462        let mut raw: ffi::RenderStateRowIterator = std::ptr::null_mut();
463        let result = unsafe { ffi::ghostty_render_state_row_iterator_new(alloc, &raw mut raw) };
464        from_result(result)?;
465        Ok(Self(Object::new(raw)?))
466    }
467
468    /// Update the row iterator for a snapshot of the render state,
469    /// returning a new row iteration.
470    pub fn update(
471        &mut self,
472        snapshot: &'_ Snapshot<'alloc, '_>,
473    ) -> Result<RowIteration<'alloc, '_>> {
474        let result = unsafe {
475            ffi::ghostty_render_state_get(
476                snapshot.0.0.as_raw(),
477                ffi::RenderStateData::ROW_ITERATOR,
478                std::ptr::from_mut(&mut self.0.ptr).cast(),
479            )
480        };
481        from_result(result)?;
482
483        Ok(RowIteration {
484            iter: self,
485            _phan: PhantomData,
486        })
487    }
488}
489
490impl Drop for RowIterator<'_> {
491    fn drop(&mut self) {
492        unsafe { ffi::ghostty_render_state_row_iterator_free(self.0.as_raw()) }
493    }
494}
495
496impl RowIteration<'_, '_> {
497    /// Move a row iteration to the next row.
498    ///
499    /// Returns `Some(row)` if the iteration moved successfully and row
500    /// data is available to read at the new position using `row`.
501    #[expect(
502        clippy::should_implement_trait,
503        reason = "lending `next` cannot implement trait"
504    )]
505    pub fn next(&mut self) -> Option<&Self> {
506        if unsafe { ffi::ghostty_render_state_row_iterator_next(self.iter.0.as_raw()) } {
507            Some(self)
508        } else {
509            None
510        }
511    }
512
513    fn get<T>(&self, tag: ffi::RenderStateRowData::Type) -> Result<T> {
514        let mut value = MaybeUninit::<T>::zeroed();
515        let result = unsafe {
516            ffi::ghostty_render_state_row_get(self.iter.0.as_raw(), tag, value.as_mut_ptr().cast())
517        };
518        // Since we manually model every possible query, this should never fail.
519        from_result(result)?;
520        // SAFETY: Value should be initialized after successful call.
521        Ok(unsafe { value.assume_init() })
522    }
523
524    fn set<T>(&self, tag: ffi::RenderStateRowOption::Type, value: &T) -> Result<()> {
525        let result = unsafe {
526            ffi::ghostty_render_state_row_set(
527                self.iter.0.as_raw(),
528                tag,
529                std::ptr::from_ref(value).cast(),
530            )
531        };
532        from_result(result)
533    }
534
535    /// Whether the current row is dirty.
536    pub fn dirty(&self) -> Result<bool> {
537        self.get(ffi::RenderStateRowData::DIRTY)
538    }
539
540    /// The raw row value.
541    pub fn raw_row(&self) -> Result<Row> {
542        self.get(ffi::RenderStateRowData::RAW).map(Row)
543    }
544
545    /// Set dirty state for the current row.
546    pub fn set_dirty(&self, dirty: bool) -> Result<()> {
547        self.set(ffi::RenderStateRowOption::DIRTY, &dirty)
548    }
549
550    /// Row-local selected cell range.
551    pub fn selection(&self) -> Result<Option<RowSelection>> {
552        let mut value = ffi::sized!(RowSelection);
553        let result = unsafe {
554            ffi::ghostty_render_state_row_get(
555                self.iter.0.as_raw(),
556                ffi::RenderStateRowData::SELECTION,
557                std::ptr::from_mut(&mut value).cast(),
558            )
559        };
560        // Since we manually model every possible query, this should never fail.
561        // SAFETY: Value should be initialized after successful call.
562        from_optional_result(result, value)
563    }
564}
565
566impl<'alloc> CellIterator<'alloc> {
567    /// Create a new cell iterator instance.
568    pub fn new() -> Result<Self> {
569        // SAFETY: A NULL allocator is always valid
570        unsafe { Self::new_inner(std::ptr::null()) }
571    }
572
573    /// Create a new cell iterator instance with a custom allocator.
574    ///
575    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
576    /// regarding custom memory management and lifetimes.
577    pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
578        // SAFETY: Borrow checking should forbid invalid allocators
579        unsafe { Self::new_inner(alloc.to_raw()) }
580    }
581
582    unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
583        let mut raw: ffi::RenderStateRowCells = std::ptr::null_mut();
584        let result = unsafe { ffi::ghostty_render_state_row_cells_new(alloc, &raw mut raw) };
585        from_result(result)?;
586        Ok(Self(Object::new(raw)?))
587    }
588
589    /// Update the cell iterator for a new row iteration,
590    /// returning a new cell iteration.
591    pub fn update(
592        &mut self,
593        row: &'_ RowIteration<'alloc, '_>,
594    ) -> Result<CellIteration<'alloc, '_>> {
595        let result = unsafe {
596            ffi::ghostty_render_state_row_get(
597                row.iter.0.as_raw(),
598                ffi::RenderStateRowData::CELLS,
599                std::ptr::from_mut(&mut self.0.ptr).cast(),
600            )
601        };
602        from_result(result)?;
603
604        Ok(CellIteration {
605            iter: self,
606            _phan: PhantomData,
607        })
608    }
609}
610
611impl Drop for CellIterator<'_> {
612    fn drop(&mut self) {
613        unsafe { ffi::ghostty_render_state_row_cells_free(self.0.as_raw()) }
614    }
615}
616
617impl CellIteration<'_, '_> {
618    /// Move a cell iteration to the next cell.
619    ///
620    /// Returns `Some(cell)` if the iteration moved successfully and cell
621    /// data is available to read at the new position using `cell`.
622    #[expect(
623        clippy::should_implement_trait,
624        reason = "lending `next` cannot implement trait"
625    )]
626    pub fn next(&mut self) -> Option<&Self> {
627        if unsafe { ffi::ghostty_render_state_row_cells_next(self.iter.0.as_raw()) } {
628            Some(self)
629        } else {
630            None
631        }
632    }
633
634    /// Move a cell iteration to a specific column.
635    ///
636    /// Positions the iteration at the given x (column) index so that
637    /// subsequent reads return data for that cell.
638    pub fn select(&mut self, x: u16) -> Result<()> {
639        let result = unsafe { ffi::ghostty_render_state_row_cells_select(self.iter.0.as_raw(), x) };
640        from_result(result)
641    }
642
643    fn get<T>(&self, tag: ffi::RenderStateRowCellsData::Type) -> Result<T> {
644        let mut value = MaybeUninit::<T>::zeroed();
645        let result = unsafe {
646            ffi::ghostty_render_state_row_cells_get(
647                self.iter.0.as_raw(),
648                tag,
649                value.as_mut_ptr().cast(),
650            )
651        };
652        from_result(result)?;
653        // SAFETY: Value should be initialized after successful call.
654        Ok(unsafe { value.assume_init() })
655    }
656
657    /// The raw cell value.
658    pub fn raw_cell(&self) -> Result<Cell> {
659        self.get(ffi::RenderStateRowCellsData::RAW).map(Cell)
660    }
661
662    /// The style for the current cell.
663    pub fn style(&self) -> Result<Style> {
664        let mut value = ffi::sized!(ffi::Style);
665        let result = unsafe {
666            ffi::ghostty_render_state_row_cells_get(
667                self.iter.0.as_raw(),
668                ffi::RenderStateRowCellsData::STYLE,
669                std::ptr::from_mut(&mut value).cast(),
670            )
671        };
672        from_result(result)?;
673        Style::try_from(value)
674    }
675
676    /// The resolved foreground color of the cell.
677    ///
678    /// Resolves palette indices through the palette. Bold color handling
679    /// is not applied; the caller should handle bold styling separately.
680    ///
681    /// Returns `None` if the cell has no explicit foreground color, in which
682    /// case the caller should use whatever default foreground color it want
683    /// (e.g. the terminal foreground).
684    pub fn fg_color(&self) -> Result<Option<RgbColor>> {
685        let res = self.get::<ffi::ColorRgb>(ffi::RenderStateRowCellsData::FG_COLOR);
686        match res {
687            Ok(o) => Ok(Some(o.into())),
688            Err(Error::InvalidValue) => Ok(None),
689            Err(e) => Err(e),
690        }
691    }
692
693    /// The resolved background color of the cell.
694    ///
695    /// Flattens the three possible sources: [`Cell::bg_color_rgb`],
696    /// [`Cell::bg_color_palette`] (looked up in the palette), or the
697    /// style's [`bg_color`][Style::bg_color].
698    ///
699    /// Returns `None` if the cell has no background color, in which case the
700    /// caller should use whatever default background color it wants
701    /// (e.g. the terminal background).
702    pub fn bg_color(&self) -> Result<Option<RgbColor>> {
703        let res = self.get::<ffi::ColorRgb>(ffi::RenderStateRowCellsData::BG_COLOR);
704        match res {
705            Ok(o) => Ok(Some(o.into())),
706            Err(Error::InvalidValue) => Ok(None),
707            Err(e) => Err(e),
708        }
709    }
710
711    /// Get the grapheme codepoints.
712    ///
713    /// The base codepoint is placed first, followed by any extra codepoints.
714    pub fn graphemes(&self) -> Result<Vec<char>> {
715        let len = self.graphemes_len()?;
716        let mut graphemes = vec!['\0'; len];
717        self.graphemes_buf(&mut graphemes)?;
718        Ok(graphemes)
719    }
720
721    /// The total number of grapheme codepoints including the base codepoint.
722    ///
723    /// Returns 0 if the cell has no text.
724    pub fn graphemes_len(&self) -> Result<usize> {
725        self.get(ffi::RenderStateRowCellsData::GRAPHEMES_LEN)
726    }
727
728    /// Write grapheme codepoints into a caller-provided buffer.
729    ///
730    /// The buffer must be at least [`CellIteration::graphemes_len`] elements.
731    /// The base codepoint is written first, followed by any extra codepoints.
732    pub fn graphemes_buf(&self, buf: &mut [char]) -> Result<()> {
733        let result = unsafe {
734            ffi::ghostty_render_state_row_cells_get(
735                self.iter.0.as_raw(),
736                ffi::RenderStateRowCellsData::GRAPHEMES_BUF,
737                buf.as_mut_ptr().cast(),
738            )
739        };
740        from_result(result)
741    }
742
743    /// Encode the current cell's full grapheme cluster as UTF-8 into a
744    /// caller-provided string buffer.
745    ///
746    /// The base codepoint is encoded first, followed by any extra grapheme
747    /// codepoints.
748    ///
749    /// May grow the buffer if more space is required.
750    pub fn graphemes_utf8(&self, buf: &mut String) -> Result<()> {
751        // SAFETY: String comes with some very stringent safety requirements,
752        // so we'll detail them here. The safety protocol for the C API is
753        // essentially that, in case of an error, no data will be written
754        // to the String's underlying buffer, and the buffer should appear
755        // as if unmodified. As such, we should be fine to operate on the
756        // original buffer directly and not cause any UB or break any
757        // invariants with the String's internal state.
758        //
759        // Since Strings do not have a `set_len` method like Vecs, in the
760        // happy path we have to recombine the entire string from its
761        // constituents, i.e. its pointer, length and capacity. This should
762        // be fine as the pointer indeed came from the original String,
763        // and that we do not attempt to copy the pointer anywhere and
764        // potentially cause aliasing issues. As for the remaining factors,
765        // we have to trust that the API will not cause length and capacity
766        // to have nonsensical values, and that the underlying bytes are
767        // indeed UTF-8.
768        //
769        // TODO: Use `String::into_raw_parts` to make this slightly simpler
770
771        let cbuf = loop {
772            // Save the old length of the String for later
773            let len = buf.len();
774            let mut cbuf = ffi::Buffer {
775                ptr: buf.as_mut_ptr(),
776                cap: buf.capacity(),
777                len,
778            };
779
780            let result = unsafe {
781                ffi::ghostty_render_state_row_cells_get(
782                    self.iter.0.as_raw(),
783                    ffi::RenderStateRowCellsData::GRAPHEMES_UTF8,
784                    std::ptr::from_mut(&mut cbuf).cast(),
785                )
786            };
787            match result {
788                ffi::Result::SUCCESS => break Ok(cbuf),
789                ffi::Result::OUT_OF_MEMORY => break Err(Error::OutOfMemory),
790                ffi::Result::OUT_OF_SPACE => {
791                    // When OutOfSpace is returned, the new length is written
792                    // to `cbuf.len`, so we reserve additional space for that
793                    buf.reserve(cbuf.len - len);
794                    continue;
795                }
796                ffi::Result::NO_VALUE | ffi::Result::INVALID_VALUE | _ => {
797                    break Err(Error::InvalidValue);
798                }
799            };
800        }?;
801
802        // Reconstitute the original String
803        // WITHOUT DROPPING THE EXISTING STRING OBJECT (!!)
804        // Otherwise, memory corruption, double frees, etc. WILL happen.
805        unsafe {
806            std::ptr::write(buf, String::from_raw_parts(cbuf.ptr, cbuf.len, cbuf.cap));
807        }
808        Ok(())
809    }
810
811    /// Whether the cell is contained within the current selection.
812    ///
813    /// This returns true when the cell's column is within the current row's
814    /// row-local selection range, and false otherwise. Rendering policy for
815    /// selected cells (colors, inversion, etc.) is left to the caller.
816    ///
817    /// Renderers that can draw cells in spans may be more efficient calling
818    /// [`RowIteration::selection`] once per row and applying that range
819    /// directly, avoiding one C API call per cell for selection state.
820    pub fn is_selected(&self) -> Result<bool> {
821        self.get(ffi::RenderStateRowCellsData::SELECTED)
822    }
823
824    /// Whether the cell has any explicit styling.
825    ///
826    /// This is equivalent to querying the raw cell's [`Cell::has_styling`]
827    /// value, but avoids materializing the raw [`Cell`] for renderers that
828    /// only need to know whether fetching the full style is necessary.
829    pub fn has_styling(&self) -> Result<bool> {
830        self.get(ffi::RenderStateRowCellsData::HAS_STYLING)
831    }
832}
833
834//---------------------------
835// Auxiliary types
836//---------------------------
837
838/// Cursor viewport position information.
839#[derive(Clone, Copy, Debug, PartialEq, Eq)]
840pub struct CursorViewport {
841    /// Cursor viewport x position in cells.
842    pub x: u16,
843    /// Cursor viewport y position in cells.
844    pub y: u16,
845    /// Whether the cursor is on the tail of a wide character.
846    pub at_wide_tail: bool,
847}
848
849/// Render-state color information.
850#[derive(Clone, Debug, PartialEq, Eq)]
851pub struct Colors {
852    /// The default/current background color for the render state.
853    pub background: RgbColor,
854    /// The default/current foreground color for the render state.
855    pub foreground: RgbColor,
856    /// The cursor color which may be explicitly set by terminal state.
857    pub cursor: Option<RgbColor>,
858    /// The active 256-color palette for this render state.
859    pub palette: [RgbColor; 256],
860}
861
862/// Dirty state of a render state after update.
863#[repr(u32)]
864#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
865pub enum Dirty {
866    /// Not dirty at all; rendering can be skipped.
867    Clean = ffi::RenderStateDirty::FALSE,
868    /// Some rows changed; renderer can redraw incrementally.
869    Partial = ffi::RenderStateDirty::PARTIAL,
870    /// Global state changed; renderer should redraw everything.
871    Full = ffi::RenderStateDirty::FULL,
872}
873
874/// Visual style of the cursor.
875#[repr(u32)]
876#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
877#[non_exhaustive]
878pub enum CursorVisualStyle {
879    /// Bar cursor (DECSCUSR 5, 6).
880    Bar = ffi::RenderStateCursorVisualStyle::BAR,
881    /// Block cursor (DECSCUSR 1, 2).
882    Block = ffi::RenderStateCursorVisualStyle::BLOCK,
883    /// Underline cursor (DECSCUSR 3, 4).
884    Underline = ffi::RenderStateCursorVisualStyle::UNDERLINE,
885    /// Hollow block cursor.
886    BlockHollow = ffi::RenderStateCursorVisualStyle::BLOCK_HOLLOW,
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use crate::terminal::{Options, Terminal};
893
894    /// Guards the `set_dirty` → `update` → `dirty()` round-trip. If
895    /// `Snapshot::set(value: &T)` calls `from_ref(&value)`, the result has
896    /// type `*const &T` (a pointer to the local reference), not `*const T`.
897    /// C reads stack-address bytes into the dirty field, the next `update`
898    /// propagates them, and `dirty()` fails enum decoding.
899    #[test]
900    fn dirty_decodes_after_set_dirty_then_update() {
901        let terminal = Terminal::new(Options {
902            cols: 8,
903            rows: 3,
904            max_scrollback: 0,
905        })
906        .unwrap();
907        let mut state = RenderState::new().unwrap();
908
909        state
910            .update(&terminal)
911            .unwrap()
912            .set_dirty(Dirty::Clean)
913            .unwrap();
914
915        assert!(state.update(&terminal).unwrap().dirty().is_ok());
916    }
917}