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}