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_result},
8    ffi,
9    screen::{Cell, Row},
10    style::{RgbColor, Style},
11    terminal::Terminal,
12};
13
14/// Represents the state required to render a visible screen (a viewport) of
15/// a terminal instance.
16///
17/// This is stateful and optimized for repeated updates from a single terminal
18/// instance and only updating dirty regions of the screen.
19///
20/// The key design principle of this API is that it only needs read/write
21/// access to the terminal instance during the update call. This allows the
22/// render state to minimally impact terminal IO performance and also allows
23/// the renderer to be safely multi-threaded (as long as a lock is held
24/// during the update call to ensure exclusive access to the terminal instance).
25///
26/// The basic usage of this API is:
27///
28///  1. Create an empty render state
29///  2. Update it from a terminal instance whenever you need.
30///  3. Read from the render state to get the data needed to draw your frame.
31///
32/// # Dirty Tracking
33///
34/// Dirty tracking is a key feature of the render state that allows renderers
35/// to efficiently determine what parts of the screen have changed and only
36/// redraw changed regions.
37///
38/// The render state API keeps track of dirty state at two independent layers:
39/// a global dirty state that indicates whether the entire frame is clean,
40/// partially dirty, or fully dirty, and a per-row dirty state that allows
41/// tracking which rows in a partially dirty frame have changed.
42///
43/// The user of the render state API is expected to unset both of these.
44/// The update call does not unset dirty state, it only updates it.
45///
46/// An extremely important detail: **setting one dirty state doesn't unset
47/// the other.** For example, setting the global dirty state to false does
48/// not reset the row-level dirty flags. So, the caller of the render state
49/// API must be careful to manage both layers of dirty state correctly.
50///
51/// # Examples
52///
53/// ## Creating and updating render state
54///
55/// ```rust
56/// // Create a terminal and render state, then update the render state
57/// // from the terminal. The render state captures a snapshot of everything
58/// // needed to draw a frame.
59/// use libghostty_vt::{Terminal, TerminalOptions, RenderState};
60///
61/// let mut terminal = Terminal::new(TerminalOptions {
62///     cols: 40,
63///     rows: 5,
64///     max_scrollback: 10000,
65/// }).unwrap();
66///
67/// let mut render_state = RenderState::new().unwrap();
68///
69/// // Feed some styled content into the terminal.
70/// terminal.vt_write(b"Hello, \x1b[1;32mworld\x1b[0m!\r\n");
71/// terminal.vt_write(b"\x1b[4munderlined\x1b[0m text\r\n");
72/// terminal.vt_write(b"\x1b[38;2;255;128;0morange\x1b[0m\r\n");
73///
74/// assert!(render_state.update(&terminal).is_ok());
75/// ```
76///
77/// ## Checking dirty state
78///
79/// ```rust
80/// // Check the global dirty state to decide how much work the renderer
81/// // needs to do. After rendering, reset it to false.
82/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState, render::Dirty};
83/// # let terminal = Terminal::new(TerminalOptions {
84/// #     cols: 80,
85/// #     rows: 25,
86/// #     max_scrollback: 10000,
87/// # }).unwrap();
88/// # let mut render_state = RenderState::new().unwrap();
89/// let snapshot = render_state.update(&terminal).unwrap();
90///
91/// match snapshot.dirty().unwrap() {
92///     Dirty::Clean => println!("Frame is clean, nothing to draw."),
93///     Dirty::Partial => println!("Partial redraw needed."),
94///     Dirty::Full => println!("Full redraw needed."),
95/// }
96/// ```
97///
98/// ## Reading colors
99///
100/// ```rust
101/// // Retrieve colors (background, foreground, palette) from the render
102/// // state. These are needed to resolve palette-indexed cell colors.
103/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState};
104/// # let terminal = Terminal::new(TerminalOptions {
105/// #     cols: 80,
106/// #     rows: 25,
107/// #     max_scrollback: 10000,
108/// # }).unwrap();
109/// # let mut render_state = RenderState::new().unwrap();
110/// let snapshot = render_state.update(&terminal).unwrap();
111/// let colors = snapshot.colors().unwrap();
112///
113/// println!(
114///     "Background: {:02x}{:02x}{:02x}",
115///     colors.background.r, colors.background.g, colors.background.b
116/// );
117/// println!(
118///     "Foreground: {:02x}{:02x}{:02x}",
119///     colors.background.r, colors.background.g, colors.background.b
120/// );
121/// ```
122///
123/// ## Reading cursor state
124///
125/// ```rust
126/// // Read cursor position and visual style from the render state.
127/// use libghostty_vt::render::CursorViewport;
128/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState};
129/// # let terminal = Terminal::new(TerminalOptions {
130/// #     cols: 80,
131/// #     rows: 25,
132/// #     max_scrollback: 10000,
133/// # }).unwrap();
134/// # let mut render_state = RenderState::new().unwrap();
135/// let snapshot = render_state.update(&terminal).unwrap();
136///
137/// if snapshot.cursor_visible().unwrap() {
138///     if let Some(CursorViewport { x, y, .. }) = snapshot.cursor_viewport().unwrap() {
139///         let style = snapshot.cursor_visual_style().unwrap();
140///         println!("Cursor at ({x}, {y}), style {style:?}");
141///     }
142/// }
143/// ```
144///
145/// ## Iterating rows and cells
146///
147/// ```rust
148/// // Iterate rows via the row iterator. For each dirty row, iterate its
149/// // cells, read codepoints/graphemes and styles, and emit ANSI-colored
150/// // output as a simple "renderer".
151/// # use libghostty_vt::{Terminal, TerminalOptions, RenderState};
152/// # let terminal = Terminal::new(TerminalOptions {
153/// #     cols: 80,
154/// #     rows: 25,
155/// #     max_scrollback: 10000,
156/// # }).unwrap();
157/// # let mut render_state = RenderState::new().unwrap();
158/// use libghostty_vt::render::{RowIterator, CellIterator};
159///
160/// // During setup:
161/// let mut rows = RowIterator::new().unwrap();
162/// let mut cells = CellIterator::new().unwrap();
163///
164/// // On each frame:
165/// let snapshot = render_state.update(&terminal).unwrap();
166/// let mut row_iter = rows.update(&snapshot);
167/// let mut row_index = 0;
168///
169/// while let Some(row) = row_iter.next() {
170///     // Check per-row dirty state; a real renderer would skip clean rows.
171///     print!(
172///         "Row {row_index} [{}]",
173///         if row.dirty().unwrap() { "dirty" } else { "clean" }
174///     );
175///
176///     // Get cells for this row (reuses the same cells handle).
177///     let mut cell_iter = cells.update(&row);
178///     while let Some(cell) = cell_iter.next() {
179///         let graphemes = cell.graphemes().unwrap();
180///         println!("{:?}", &graphemes);
181///     }
182///     row_index += 1;
183///     println!()
184/// }
185/// ```
186#[derive(Debug)]
187pub struct RenderState<'alloc>(Object<'alloc, ffi::GhosttyRenderState>);
188
189/// A snapshot of the render state after an update.
190///
191/// This struct exists to guard data accessed from the render state from
192/// being accidentally modified after an update. If you find yourself unable
193/// to update the render state due to borrow checker errors, make sure to
194/// drop the active snapshot (and data that depends on it) before updating.
195#[derive(Debug)]
196pub struct Snapshot<'alloc, 's>(&'s mut RenderState<'alloc>);
197
198/// Opaque handle to a render-state row iterator.
199///
200/// The row iterator must be [updated](RowIterator::update) from a snapshot of
201/// the render state in order to function, as most data is only accessible
202/// per [iteration](RowIteration).
203#[derive(Debug)]
204pub struct RowIterator<'alloc>(Object<'alloc, ffi::GhosttyRenderStateRowIterator>);
205
206/// An active iteration over the rows in the render state.
207///
208/// Row iterations are created by [updating](RowIterator::update) row iterators
209/// with a snapshot of the render state. The borrow checker statically
210/// guarantees that all accesses of the data do not outlive the given snapshot,
211/// at the cost of added lifetime annotations.
212#[derive(Debug)]
213pub struct RowIteration<'alloc, 's> {
214    iter: &'s mut RowIterator<'alloc>,
215    // NOTE: While in theory the snapshot borrow should have its own
216    // lifetime 'ss where 'rs: 'ss, but it gets very unwieldy and honestly
217    // one wouldn't run into too many situations where this simpler constraint
218    // isn't enough.
219    _phan: PhantomData<&'s Snapshot<'alloc, 's>>,
220}
221
222/// Opaque handle to a render state cell iterator.
223///
224/// The cell iterator must be [updated](CellIterator::update) from a
225/// [row](RowIteration) in order to function, as most data is only
226/// accessible per [iteration](CellIteration).
227#[derive(Debug)]
228pub struct CellIterator<'alloc>(Object<'alloc, ffi::GhosttyRenderStateRowCells>);
229
230/// An active iteration over the cells on a given row
231/// within the render state.
232///
233/// Cell iterations are created by [updating](CellIterator::update) row iterators
234/// at a given [row](RowIteration). The borrow checker statically
235/// guarantees that all accesses of the data do not outlive the given snapshot,
236/// at the cost of added lifetime annotations.
237#[derive(Debug)]
238pub struct CellIteration<'alloc, 's> {
239    iter: &'s mut CellIterator<'alloc>,
240    _phan: PhantomData<&'s RowIteration<'alloc, 's>>,
241}
242
243//--------------------------
244// Impl blocks
245//--------------------------
246
247impl<'alloc> RenderState<'alloc> {
248    /// Create a new render state instance.
249    pub fn new() -> Result<Self> {
250        // SAFETY: A NULL allocator is always valid
251        unsafe { Self::new_inner(std::ptr::null()) }
252    }
253
254    /// Create a new render state instance with a custom allocator.
255    ///
256    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
257    /// regarding custom memory management and lifetimes.
258    pub fn new_with_alloc<'ctx: 'alloc, Ctx>(alloc: &'alloc Allocator<'ctx, Ctx>) -> Result<Self> {
259        // SAFETY: Borrow checking should forbid invalid allocators
260        unsafe { Self::new_inner(alloc.to_raw()) }
261    }
262
263    unsafe fn new_inner(alloc: *const ffi::GhosttyAllocator) -> Result<Self> {
264        let mut raw: ffi::GhosttyRenderState_ptr = std::ptr::null_mut();
265        let result = unsafe { ffi::ghostty_render_state_new(alloc, &raw mut raw) };
266        from_result(result)?;
267        Ok(Self(Object::new(raw)?))
268    }
269
270    /// Update a render state instance from a terminal,
271    /// returning a new [snapshot](Snapshot).
272    ///
273    /// This consumes terminal/screen dirty state in the same way as the
274    /// internal render state update path.
275    ///
276    /// # Errors
277    ///
278    /// Returns `Err(Error::OutOfMemory)` if updating the state requires
279    /// allocation and that allocation fails.
280    pub fn update<'cb>(
281        &mut self,
282        terminal: &Terminal<'alloc, 'cb>,
283    ) -> Result<Snapshot<'alloc, '_>> {
284        let result =
285            unsafe { ffi::ghostty_render_state_update(self.0.as_raw(), terminal.inner.as_raw()) };
286        from_result(result)?;
287        Ok(Snapshot(self))
288    }
289}
290
291impl Drop for RenderState<'_> {
292    fn drop(&mut self) {
293        unsafe { ffi::ghostty_render_state_free(self.0.as_raw()) }
294    }
295}
296
297impl Snapshot<'_, '_> {
298    fn get<T>(&self, tag: ffi::GhosttyRenderStateData) -> Result<T> {
299        let mut value = MaybeUninit::<T>::zeroed();
300        let result = unsafe {
301            ffi::ghostty_render_state_get(self.0.0.as_raw(), tag, value.as_mut_ptr().cast())
302        };
303        // Since we manually model every possible query, this should never fail.
304        from_result(result)?;
305        // SAFETY: Value should be initialized after successful call.
306        Ok(unsafe { value.assume_init() })
307    }
308
309    fn set<T>(&self, tag: ffi::GhosttyRenderStateOption, value: &T) -> Result<()> {
310        let result = unsafe {
311            ffi::ghostty_render_state_set(self.0.0.as_raw(), tag, std::ptr::from_ref(&value).cast())
312        };
313        // Since we manually model every possible query, this should never fail.
314        from_result(result)
315    }
316
317    /// Get the current dirty state.
318    pub fn dirty(&self) -> Result<Dirty> {
319        self.get::<ffi::GhosttyRenderStateDirty>(
320            ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_DIRTY,
321        )
322        .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
323    }
324
325    /// Get the viewport width.
326    pub fn cols(&self) -> Result<u16> {
327        self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLS)
328    }
329
330    /// Get the viewport height.
331    pub fn rows(&self) -> Result<u16> {
332        self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROWS)
333    }
334
335    /// Get the cursor color that may have been explicitly set by the terminal state.
336    pub fn cursor_color(&self) -> Result<Option<RgbColor>> {
337        let has_value =
338            self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE)?;
339        if has_value {
340            let color =
341                self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR)?;
342            Ok(Some(color))
343        } else {
344            Ok(None)
345        }
346    }
347
348    /// Whether the cursor is currently visible based on terminal modes.
349    pub fn cursor_visible(&self) -> Result<bool> {
350        self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE)
351    }
352
353    /// Whether the cursor is currently blinking based on terminal modes.
354    pub fn cursor_blinking(&self) -> Result<bool> {
355        self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING)
356    }
357
358    /// Whether the cursor is at a password input field.
359    pub fn cursor_password_input(&self) -> Result<bool> {
360        self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT)
361    }
362
363    /// Get the visual style of the cursor.
364    pub fn cursor_visual_style(&self) -> Result<CursorVisualStyle> {
365        self.get::<ffi::GhosttyRenderStateCursorVisualStyle>(
366            ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE,
367        )
368        .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
369    }
370
371    /// Get the relative position of the cursor and other information
372    /// if it is currently visible within the viewport.
373    pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>> {
374        let has_value = self
375            .get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE)?;
376        if has_value {
377            let x =
378                self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X)?;
379            let y =
380                self.get(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y)?;
381            let at_wide_tail = self.get(
382                ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
383            )?;
384            Ok(Some(CursorViewport { x, y, at_wide_tail }))
385        } else {
386            Ok(None)
387        }
388    }
389
390    /// Get the current color information from a render state.
391    pub fn colors(&self) -> Result<Colors> {
392        let mut colors = ffi::sized!(ffi::GhosttyRenderStateColors);
393        let result =
394            unsafe { ffi::ghostty_render_state_colors_get(self.0.0.as_raw(), &raw mut colors) };
395        from_result(result)?;
396
397        Ok(Colors {
398            background: colors.background.into(),
399            foreground: colors.foreground.into(),
400            cursor: if colors.cursor_has_value {
401                Some(colors.cursor.into())
402            } else {
403                None
404            },
405            palette: colors.palette.map(Into::into),
406        })
407    }
408
409    /// Set dirty state.
410    pub fn set_dirty(&self, dirty: Dirty) -> Result<()> {
411        self.set(
412            ffi::GhosttyRenderStateOption_GHOSTTY_RENDER_STATE_OPTION_DIRTY,
413            &ffi::GhosttyRenderStateDirty::from(dirty),
414        )
415    }
416}
417
418impl<'alloc> RowIterator<'alloc> {
419    /// Create a new row iterator instance.
420    pub fn new() -> Result<Self> {
421        // SAFETY: A NULL allocator is always valid
422        unsafe { Self::new_inner(std::ptr::null()) }
423    }
424
425    /// Create a new cell iterator instance with a custom allocator.
426    ///
427    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
428    /// regarding custom memory management and lifetimes.
429    pub fn new_with_alloc<'ctx: 'alloc, Ctx>(alloc: &'alloc Allocator<'ctx, Ctx>) -> Result<Self> {
430        // SAFETY: Borrow checking should forbid invalid allocators
431        unsafe { Self::new_inner(alloc.to_raw()) }
432    }
433
434    unsafe fn new_inner(alloc: *const ffi::GhosttyAllocator) -> Result<Self> {
435        let mut raw: ffi::GhosttyRenderStateRowIterator_ptr = std::ptr::null_mut();
436        let result = unsafe { ffi::ghostty_render_state_row_iterator_new(alloc, &raw mut raw) };
437        from_result(result)?;
438        Ok(Self(Object::new(raw)?))
439    }
440
441    /// Update the row iterator for a snapshot of the render state,
442    /// returning a new row iteration.
443    pub fn update(
444        &mut self,
445        snapshot: &'_ Snapshot<'alloc, '_>,
446    ) -> Result<RowIteration<'alloc, '_>> {
447        let result = unsafe {
448            ffi::ghostty_render_state_get(
449                snapshot.0.0.as_raw(),
450                ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR,
451                std::ptr::from_mut(&mut self.0.ptr).cast(),
452            )
453        };
454        from_result(result)?;
455
456        Ok(RowIteration {
457            iter: self,
458            _phan: PhantomData,
459        })
460    }
461}
462
463impl Drop for RowIterator<'_> {
464    fn drop(&mut self) {
465        unsafe { ffi::ghostty_render_state_row_iterator_free(self.0.as_raw()) }
466    }
467}
468
469impl RowIteration<'_, '_> {
470    /// Move a row iteration to the next row.
471    ///
472    /// Returns `Some(row)` if the iteration moved successfully and row
473    /// data is available to read at the new position using `row`.
474    #[expect(
475        clippy::should_implement_trait,
476        reason = "lending `next` cannot implement trait"
477    )]
478    pub fn next(&mut self) -> Option<&Self> {
479        if unsafe { ffi::ghostty_render_state_row_iterator_next(self.iter.0.as_raw()) } {
480            Some(self)
481        } else {
482            None
483        }
484    }
485
486    fn get<T>(&self, tag: ffi::GhosttyRenderStateRowData) -> Result<T> {
487        let mut value = MaybeUninit::<T>::zeroed();
488        let result = unsafe {
489            ffi::ghostty_render_state_row_get(self.iter.0.as_raw(), tag, value.as_mut_ptr().cast())
490        };
491        // Since we manually model every possible query, this should never fail.
492        from_result(result)?;
493        // SAFETY: Value should be initialized after successful call.
494        Ok(unsafe { value.assume_init() })
495    }
496
497    fn set<T>(&self, tag: ffi::GhosttyRenderStateRowOption, value: &T) -> Result<()> {
498        let result = unsafe {
499            ffi::ghostty_render_state_row_set(
500                self.iter.0.as_raw(),
501                tag,
502                std::ptr::from_ref(&value).cast(),
503            )
504        };
505        from_result(result)
506    }
507
508    /// Whether the current row is dirty.
509    pub fn dirty(&self) -> Result<bool> {
510        self.get(ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY)
511    }
512
513    /// The raw row value.
514    pub fn raw_row(&self) -> Result<Row> {
515        self.get(ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_RAW)
516            .map(Row)
517    }
518
519    /// Set dirty state for the current row.
520    pub fn set_dirty(&self, dirty: bool) -> Result<()> {
521        self.set(
522            ffi::GhosttyRenderStateRowOption_GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY,
523            &dirty,
524        )
525    }
526}
527
528impl<'alloc> CellIterator<'alloc> {
529    /// Create a new cell iterator instance.
530    pub fn new() -> Result<Self> {
531        // SAFETY: A NULL allocator is always valid
532        unsafe { Self::new_inner(std::ptr::null()) }
533    }
534
535    /// Create a new cell iterator instance with a custom allocator.
536    ///
537    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
538    /// regarding custom memory management and lifetimes.
539    pub fn new_with_alloc<'ctx: 'alloc, Ctx>(alloc: &'alloc Allocator<'ctx, Ctx>) -> Result<Self> {
540        // SAFETY: Borrow checking should forbid invalid allocators
541        unsafe { Self::new_inner(alloc.to_raw()) }
542    }
543
544    unsafe fn new_inner(alloc: *const ffi::GhosttyAllocator) -> Result<Self> {
545        let mut raw: ffi::GhosttyRenderStateRowCells_ptr = std::ptr::null_mut();
546        let result = unsafe { ffi::ghostty_render_state_row_cells_new(alloc, &raw mut raw) };
547        from_result(result)?;
548        Ok(Self(Object::new(raw)?))
549    }
550
551    /// Update the cell iterator for a new row iteration,
552    /// returning a new cell iteration.
553    pub fn update(
554        &mut self,
555        row: &'_ RowIteration<'alloc, '_>,
556    ) -> Result<CellIteration<'alloc, '_>> {
557        let result = unsafe {
558            ffi::ghostty_render_state_row_get(
559                row.iter.0.as_raw(),
560                ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_CELLS,
561                std::ptr::from_mut(&mut self.0.ptr).cast(),
562            )
563        };
564        from_result(result)?;
565
566        Ok(CellIteration {
567            iter: self,
568            _phan: PhantomData,
569        })
570    }
571}
572
573impl Drop for CellIterator<'_> {
574    fn drop(&mut self) {
575        unsafe { ffi::ghostty_render_state_row_cells_free(self.0.as_raw()) }
576    }
577}
578
579impl CellIteration<'_, '_> {
580    /// Move a cell iteration to the next cell.
581    ///
582    /// Returns `Some(cell)` if the iteration moved successfully and cell
583    /// data is available to read at the new position using `cell`.
584    #[expect(
585        clippy::should_implement_trait,
586        reason = "lending `next` cannot implement trait"
587    )]
588    pub fn next(&mut self) -> Option<&Self> {
589        if unsafe { ffi::ghostty_render_state_row_cells_next(self.iter.0.as_raw()) } {
590            Some(self)
591        } else {
592            None
593        }
594    }
595
596    /// Move a cell iteration to a specific column.
597    ///
598    /// Positions the iteration at the given x (column) index so that
599    /// subsequent reads return data for that cell.
600    pub fn select(&mut self, x: u16) -> Result<()> {
601        let result = unsafe { ffi::ghostty_render_state_row_cells_select(self.iter.0.as_raw(), x) };
602        from_result(result)
603    }
604
605    fn get<T>(&self, tag: ffi::GhosttyRenderStateRowCellsData) -> Result<T> {
606        let mut value = MaybeUninit::<T>::zeroed();
607        let result = unsafe {
608            ffi::ghostty_render_state_row_cells_get(
609                self.iter.0.as_raw(),
610                tag,
611                value.as_mut_ptr().cast(),
612            )
613        };
614        from_result(result)?;
615        // SAFETY: Value should be initialized after successful call.
616        Ok(unsafe { value.assume_init() })
617    }
618
619    /// The raw cell value.
620    pub fn raw_cell(&self) -> Result<Cell> {
621        self.get(ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW)
622            .map(Cell)
623    }
624
625    /// The style for the current cell.
626    pub fn style(&self) -> Result<Style> {
627        let mut value = ffi::sized!(ffi::GhosttyStyle);
628        let result = unsafe {
629            ffi::ghostty_render_state_row_cells_get(
630                self.iter.0.as_raw(),
631                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
632                std::ptr::from_mut(&mut value).cast(),
633            )
634        };
635        from_result(result)?;
636        Style::try_from(value)
637    }
638
639    /// The resolved foreground color of the cell.
640    ///
641    /// Resolves palette indices through the palette. Bold color handling
642    /// is not applied; the caller should handle bold styling separately.
643    ///
644    /// Returns `None` if the cell has no explicit foreground color, in which
645    /// case the caller should use whatever default foreground color it want
646    /// (e.g. the terminal foreground).
647    pub fn fg_color(&self) -> Result<Option<RgbColor>> {
648        let res = self.get::<ffi::GhosttyColorRgb>(
649            ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR,
650        );
651        match res {
652            Ok(o) => Ok(Some(o.into())),
653            Err(Error::InvalidValue) => Ok(None),
654            Err(e) => Err(e),
655        }
656    }
657
658    /// The resolved background color of the cell.
659    ///
660    /// Flattens the three possible sources: [`Cell::bg_color_rgb`],
661    /// [`Cell::bg_color_palette`] (looked up in the palette), or the
662    /// style's [`bg_color`][Style::bg_color].
663    ///
664    /// Returns `None` if the cell has no background color, in which case the
665    /// caller should use whatever default background color it wants
666    /// (e.g. the terminal background).
667    pub fn bg_color(&self) -> Result<Option<RgbColor>> {
668        let res = self.get::<ffi::GhosttyColorRgb>(
669            ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR,
670        );
671        match res {
672            Ok(o) => Ok(Some(o.into())),
673            Err(Error::InvalidValue) => Ok(None),
674            Err(e) => Err(e),
675        }
676    }
677
678    /// Get the grapheme codepoints.
679    ///
680    /// The base codepoint is placed first, followed by any extra codepoints.
681    pub fn graphemes(&self) -> Result<Vec<char>> {
682        let len = self.graphemes_len()?;
683        let mut graphemes = vec!['\0'; len];
684        self.graphemes_buf(&mut graphemes)?;
685        Ok(graphemes)
686    }
687
688    /// The total number of grapheme codepoints including the base codepoint.
689    ///
690    /// Returns 0 if the cell has no text.
691    pub fn graphemes_len(&self) -> Result<usize> {
692        self.get(
693            ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN,
694        )
695    }
696
697    /// Write grapheme codepoints into a caller-provided buffer.
698    ///
699    /// The buffer must be at least [`CellIteration::graphemes_len`] elements.
700    /// The base codepoint is written first, followed by any extra codepoints.
701    pub fn graphemes_buf(&self, buf: &mut [char]) -> Result<()> {
702        let result = unsafe {
703            ffi::ghostty_render_state_row_cells_get(
704                self.iter.0.as_raw(),
705                ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF,
706                buf.as_mut_ptr().cast(),
707            )
708        };
709        from_result(result)
710    }
711}
712
713//---------------------------
714// Auxiliary types
715//---------------------------
716
717/// Cursor viewport position information.
718#[derive(Clone, Copy, Debug, PartialEq, Eq)]
719pub struct CursorViewport {
720    /// Cursor viewport x position in cells.
721    pub x: u16,
722    /// Cursor viewport y position in cells.
723    pub y: u16,
724    /// Whether the cursor is on the tail of a wide character.
725    pub at_wide_tail: bool,
726}
727
728/// Render-state color information.
729#[derive(Clone, Debug, PartialEq, Eq)]
730pub struct Colors {
731    /// The default/current background color for the render state.
732    pub background: RgbColor,
733    /// The default/current foreground color for the render state.
734    pub foreground: RgbColor,
735    /// The cursor color which may be explicitly set by terminal state.
736    pub cursor: Option<RgbColor>,
737    /// The active 256-color palette for this render state.
738    pub palette: [RgbColor; 256],
739}
740
741/// Dirty state of a render state after update.
742#[repr(u32)]
743#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
744pub enum Dirty {
745    /// Not dirty at all; rendering can be skipped.
746    Clean = ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE,
747    /// Some rows changed; renderer can redraw incrementally.
748    Partial = ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_PARTIAL,
749    /// Global state changed; renderer should redraw everything.
750    Full = ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FULL,
751}
752
753/// Visual style of the cursor.
754#[repr(u32)]
755#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
756#[non_exhaustive]
757pub enum CursorVisualStyle {
758    /// Bar cursor (DECSCUSR 5, 6).
759    Bar = ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR,
760    /// Block cursor (DECSCUSR 1, 2).
761    Block = ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK,
762    /// Underline cursor (DECSCUSR 3, 4).
763    Underline =
764        ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE,
765    /// Hollow block cursor.
766    BlockHollow = ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW,
767}