pub struct RenderState<'alloc>(/* private fields */);Expand description
Represents the state required to render a visible screen (a viewport) of a terminal instance.
This is stateful and optimized for repeated updates from a single terminal instance and only updating dirty regions of the screen.
The key design principle of this API is that it only needs read/write access to the terminal instance during the update call. This allows the render state to minimally impact terminal IO performance and also allows the renderer to be safely multi-threaded (as long as a lock is held during the update call to ensure exclusive access to the terminal instance).
The basic usage of this API is:
- Create an empty render state
- Update it from a terminal instance whenever you need.
- Read from the render state to get the data needed to draw your frame.
§Dirty Tracking
Dirty tracking is a key feature of the render state that allows renderers to efficiently determine what parts of the screen have changed and only redraw changed regions.
The render state API keeps track of dirty state at two independent layers: a global dirty state that indicates whether the entire frame is clean, partially dirty, or fully dirty, and a per-row dirty state that allows tracking which rows in a partially dirty frame have changed.
The user of the render state API is expected to unset both of these. The update call does not unset dirty state, it only updates it.
An extremely important detail: setting one dirty state doesn’t unset the other. For example, setting the global dirty state to false does not reset the row-level dirty flags. So, the caller of the render state API must be careful to manage both layers of dirty state correctly.
§Examples
§Creating and updating render state
// Create a terminal and render state, then update the render state
// from the terminal. The render state captures a snapshot of everything
// needed to draw a frame.
use libghostty_vt::{Terminal, TerminalOptions, RenderState};
let mut terminal = Terminal::new(TerminalOptions {
cols: 40,
rows: 5,
max_scrollback: 10000,
}).unwrap();
let mut render_state = RenderState::new().unwrap();
// Feed some styled content into the terminal.
terminal.vt_write(b"Hello, \x1b[1;32mworld\x1b[0m!\r\n");
terminal.vt_write(b"\x1b[4munderlined\x1b[0m text\r\n");
terminal.vt_write(b"\x1b[38;2;255;128;0morange\x1b[0m\r\n");
assert!(render_state.update(&terminal).is_ok());§Splitting an update
use libghostty_vt::{RenderState, Terminal, TerminalOptions};
let terminal = Terminal::new(TerminalOptions {
cols: 80,
rows: 25,
max_scrollback: 10000,
})?;
let mut render_state = RenderState::new()?;
// Use `update` unless you need to minimize how long terminal access is
// held. `begin_update` copies the terminal-dependent state into an update
// token, then `end` finishes the deferred render-state work.
let update = render_state.begin_update(&terminal)?;
// Terminal access is no longer needed here.
let snapshot = update.end()?;
// Read from the snapshot to draw the frame.
let _dirty = snapshot.dirty()?;§Checking dirty state
// Check the global dirty state to decide how much work the renderer
// needs to do. After rendering, reset it to false.
let snapshot = render_state.update(&terminal).unwrap();
match snapshot.dirty().unwrap() {
Dirty::Clean => println!("Frame is clean, nothing to draw."),
Dirty::Partial => println!("Partial redraw needed."),
Dirty::Full => println!("Full redraw needed."),
}§Reading colors
// Retrieve colors (background, foreground, palette) from the render
// state. These are needed to resolve palette-indexed cell colors.
let snapshot = render_state.update(&terminal).unwrap();
let colors = snapshot.colors().unwrap();
println!(
"Background: {:02x}{:02x}{:02x}",
colors.background.r, colors.background.g, colors.background.b
);
println!(
"Foreground: {:02x}{:02x}{:02x}",
colors.background.r, colors.background.g, colors.background.b
);§Reading cursor state
// Read cursor position and visual style from the render state.
use libghostty_vt::render::CursorViewport;
let snapshot = render_state.update(&terminal).unwrap();
if snapshot.cursor_visible().unwrap() {
if let Some(CursorViewport { x, y, .. }) = snapshot.cursor_viewport().unwrap() {
let style = snapshot.cursor_visual_style().unwrap();
println!("Cursor at ({x}, {y}), style {style:?}");
}
}§Iterating rows and cells
// Iterate rows via the row iterator. For each dirty row, iterate its
// cells, read codepoints/graphemes and styles, and emit ANSI-colored
// output as a simple "renderer".
use libghostty_vt::{Terminal, TerminalOptions, RenderState};
use libghostty_vt::style::Underline;
use libghostty_vt::render::{RowIterator, CellIterator};
// During setup:
let mut rows = RowIterator::new()?;
let mut cells = CellIterator::new()?;
// On each frame:
let snapshot = render_state.update(&terminal)?;
let colors = snapshot.colors()?;
let mut row_iter = rows.update(&snapshot)?;
let mut row_index = 0;
while let Some(row) = row_iter.next() {
// Check per-row dirty state; a real renderer would skip clean rows.
print!(
"Row {row_index} [{}]",
if row.dirty()? { "dirty" } else { "clean" }
);
// Get cells for this row (reuses the same cells handle).
let mut cell_iter = cells.update(&row)?;
while let Some(cell) = cell_iter.next() {
let graphemes = cell.graphemes()?;
if graphemes.is_empty() {
print!(" ");
continue;
}
// Resolve foreground color for this cell.
let fg = cell.fg_color()?.unwrap_or(colors.foreground);
// Emit ANSI true-color escape for the foreground.
print!("\x1b[38;2;{};{};{}m", fg.r, fg.g, fg.b);
// Read the style for this cell. Returns the default style for
// cells that have no explicit styling.
let style = cell.style()?;
if style.bold {
print!("\x1b[1m");
}
if style.underline != Underline::None {
print!("\x1b[4m");
}
for grapheme in graphemes {
print!("{}", grapheme.escape_default());
}
print!("\x1b[0m"); // Reset style after each cell.
}
println!();
// Clear per-row dirty flag after "rendering" it.
row.set_dirty(false);
row_index += 1;
}Implementations§
Source§impl<'alloc> RenderState<'alloc>
impl<'alloc> RenderState<'alloc>
Sourcepub fn new_with_alloc<'ctx: 'alloc>(
alloc: &'alloc Allocator<'ctx>,
) -> Result<Self>
pub fn new_with_alloc<'ctx: 'alloc>( alloc: &'alloc Allocator<'ctx>, ) -> Result<Self>
Create a new render state instance with a custom allocator.
See the crate-level documentation regarding custom memory management and lifetimes.
Sourcepub fn update<'cb>(
&mut self,
terminal: &Terminal<'alloc, 'cb>,
) -> Result<Snapshot<'alloc, '_>>
pub fn update<'cb>( &mut self, terminal: &Terminal<'alloc, 'cb>, ) -> Result<Snapshot<'alloc, '_>>
Sourcepub fn begin_update<'cb>(
&mut self,
terminal: &Terminal<'alloc, 'cb>,
) -> Result<Update<'alloc, '_>>
pub fn begin_update<'cb>( &mut self, terminal: &Terminal<'alloc, 'cb>, ) -> Result<Update<'alloc, '_>>
Begin an update of a render state instance from a terminal.
Every begin must be completed with Update::end before the render
state is read.
This two-phase structure exists for callers that synchronize access to
the terminal state: only this function requires terminal access, so a
caller can hold its lock for this call only and then call Update::end
after releasing it. The end phase exclusively reads
and writes memory owned by the render state, so it is safe to call while
the terminal is being modified.
Work that doesn’t require terminal access may be deferred to the end
phase to keep this call, and therefore lock hold time, as short as
possible. Callers must treat the render state as incomplete until
Update::end is called.
This consumes terminal and screen dirty state in the same way as the internal render state update path.