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