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