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