use crate::error::{Error, check};
use crate::screen::RawCell;
use crate::style::{RgbColor, Style};
use crate::terminal::Terminal;
use libghostty_vt_sys as sys;
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::ptr;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Dirty {
Clean,
Partial,
Full,
}
impl From<sys::GhosttyRenderStateDirty> for Dirty {
fn from(d: sys::GhosttyRenderStateDirty) -> Self {
match d {
sys::GhosttyRenderStateDirty::GHOSTTY_RENDER_STATE_DIRTY_FALSE => Dirty::Clean,
sys::GhosttyRenderStateDirty::GHOSTTY_RENDER_STATE_DIRTY_PARTIAL => Dirty::Partial,
sys::GhosttyRenderStateDirty::GHOSTTY_RENDER_STATE_DIRTY_FULL => Dirty::Full,
_ => Dirty::Full,
}
}
}
impl From<Dirty> for sys::GhosttyRenderStateDirty {
fn from(d: Dirty) -> Self {
match d {
Dirty::Clean => sys::GhosttyRenderStateDirty::GHOSTTY_RENDER_STATE_DIRTY_FALSE,
Dirty::Partial => sys::GhosttyRenderStateDirty::GHOSTTY_RENDER_STATE_DIRTY_PARTIAL,
Dirty::Full => sys::GhosttyRenderStateDirty::GHOSTTY_RENDER_STATE_DIRTY_FULL,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct CursorViewport {
pub x: u16,
pub y: u16,
pub wide_tail: bool,
}
#[derive(Debug, Clone)]
pub struct Colors {
pub background: RgbColor,
pub foreground: RgbColor,
pub cursor: RgbColor,
pub cursor_has_value: bool,
pub palette: [RgbColor; 256],
}
pub struct RenderState<'alloc> {
handle: sys::GhosttyRenderState,
_marker: PhantomData<&'alloc ()>,
}
impl<'alloc> RenderState<'alloc> {
pub fn new() -> Result<Self, Error> {
let mut handle: sys::GhosttyRenderState = ptr::null_mut();
let r = unsafe { sys::ghostty_render_state_new(ptr::null(), &mut handle) };
check(r)?;
Ok(RenderState {
handle,
_marker: PhantomData,
})
}
pub fn update<'rs>(&'rs mut self, term: &Terminal<'_, '_>) -> Result<Snapshot<'rs>, Error> {
let r = unsafe { sys::ghostty_render_state_update(self.handle, term.as_raw()) };
check(r)?;
Ok(Snapshot {
handle: self.handle,
_marker: PhantomData,
})
}
}
impl Drop for RenderState<'_> {
fn drop(&mut self) {
unsafe {
sys::ghostty_render_state_free(self.handle);
}
}
}
pub struct Snapshot<'rs> {
handle: sys::GhosttyRenderState,
_marker: PhantomData<&'rs mut ()>,
}
impl<'rs> Snapshot<'rs> {
pub fn dirty(&self) -> Result<Dirty, Error> {
let mut out = MaybeUninit::<sys::GhosttyRenderStateDirty>::uninit();
let r = unsafe {
sys::ghostty_render_state_get(
self.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_DIRTY,
out.as_mut_ptr().cast(),
)
};
check(r)?;
Ok(unsafe { out.assume_init() }.into())
}
pub fn set_dirty(&self, d: Dirty) -> Result<(), Error> {
let val: sys::GhosttyRenderStateDirty = d.into();
let r = unsafe {
sys::ghostty_render_state_set(
self.handle,
sys::GhosttyRenderStateOption::GHOSTTY_RENDER_STATE_OPTION_DIRTY,
&val as *const _ as *const _,
)
};
check(r)
}
pub fn cursor_visible(&self) -> Result<bool, Error> {
let mut out = MaybeUninit::<bool>::uninit();
let r = unsafe {
sys::ghostty_render_state_get(
self.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE,
out.as_mut_ptr().cast(),
)
};
check(r)?;
Ok(unsafe { out.assume_init() })
}
pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>, Error> {
let mut has_value = MaybeUninit::<bool>::uninit();
let r = unsafe {
sys::ghostty_render_state_get(
self.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
has_value.as_mut_ptr().cast(),
)
};
check(r)?;
if !unsafe { has_value.assume_init() } {
return Ok(None);
}
let mut x = MaybeUninit::<u16>::uninit();
let mut y = MaybeUninit::<u16>::uninit();
let mut wide_tail = MaybeUninit::<bool>::uninit();
let r = unsafe {
sys::ghostty_render_state_get(
self.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X,
x.as_mut_ptr().cast(),
)
};
check(r)?;
let r = unsafe {
sys::ghostty_render_state_get(
self.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y,
y.as_mut_ptr().cast(),
)
};
check(r)?;
let r = unsafe {
sys::ghostty_render_state_get(
self.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
wide_tail.as_mut_ptr().cast(),
)
};
check(r)?;
Ok(Some(CursorViewport {
x: unsafe { x.assume_init() },
y: unsafe { y.assume_init() },
wide_tail: unsafe { wide_tail.assume_init() },
}))
}
pub fn colors(&self) -> Result<Colors, Error> {
let mut c = MaybeUninit::<sys::GhosttyRenderStateColors>::zeroed();
unsafe { (*c.as_mut_ptr()).size = std::mem::size_of::<sys::GhosttyRenderStateColors>() };
let r = unsafe { sys::ghostty_render_state_colors_get(self.handle, c.as_mut_ptr()) };
check(r)?;
let c = unsafe { c.assume_init() };
let palette: [RgbColor; 256] = std::array::from_fn(|i| RgbColor::from(c.palette[i]));
Ok(Colors {
background: c.background.into(),
foreground: c.foreground.into(),
cursor: c.cursor.into(),
cursor_has_value: c.cursor_has_value,
palette,
})
}
}
pub struct RowIterator {
handle: sys::GhosttyRenderStateRowIterator,
}
impl RowIterator {
pub fn new() -> Result<Self, Error> {
let mut handle: sys::GhosttyRenderStateRowIterator = ptr::null_mut();
let r = unsafe { sys::ghostty_render_state_row_iterator_new(ptr::null(), &mut handle) };
check(r)?;
Ok(RowIterator { handle })
}
pub fn update<'i, 'rs>(
&'i mut self,
snapshot: &'i Snapshot<'rs>,
) -> Result<RowIter<'i>, Error> {
let r = unsafe {
sys::ghostty_render_state_get(
snapshot.handle,
sys::GhosttyRenderStateData::GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR,
&mut self.handle as *mut _ as *mut _,
)
};
check(r)?;
Ok(RowIter {
handle: self.handle,
_marker: PhantomData,
})
}
}
impl Drop for RowIterator {
fn drop(&mut self) {
unsafe {
sys::ghostty_render_state_row_iterator_free(self.handle);
}
}
}
pub struct RowIter<'i> {
handle: sys::GhosttyRenderStateRowIterator,
_marker: PhantomData<&'i mut ()>,
}
impl<'i> RowIter<'i> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<Row<'_>> {
let ok = unsafe { sys::ghostty_render_state_row_iterator_next(self.handle) };
if ok {
Some(Row {
handle: self.handle,
_marker: PhantomData,
})
} else {
None
}
}
}
pub struct Row<'i> {
handle: sys::GhosttyRenderStateRowIterator,
_marker: PhantomData<&'i mut ()>,
}
impl<'i> Row<'i> {
pub fn dirty(&self) -> Result<bool, Error> {
let mut out = MaybeUninit::<bool>::uninit();
let r = unsafe {
sys::ghostty_render_state_row_get(
self.handle,
sys::GhosttyRenderStateRowData::GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY,
out.as_mut_ptr().cast(),
)
};
check(r)?;
Ok(unsafe { out.assume_init() })
}
pub fn set_dirty(&self, dirty: bool) -> Result<(), Error> {
let val: bool = dirty;
let r = unsafe {
sys::ghostty_render_state_row_set(
self.handle,
sys::GhosttyRenderStateRowOption::GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY,
&val as *const _ as *const _,
)
};
check(r)
}
pub(crate) fn as_raw(&self) -> sys::GhosttyRenderStateRowIterator {
self.handle
}
}
pub struct CellIterator {
handle: sys::GhosttyRenderStateRowCells,
}
impl CellIterator {
pub fn new() -> Result<Self, Error> {
let mut handle: sys::GhosttyRenderStateRowCells = ptr::null_mut();
let r = unsafe { sys::ghostty_render_state_row_cells_new(ptr::null(), &mut handle) };
check(r)?;
Ok(CellIterator { handle })
}
pub fn update<'i>(&'i mut self, row: &'i Row<'_>) -> Result<CellIter<'i>, Error> {
let r = unsafe {
sys::ghostty_render_state_row_get(
row.as_raw(),
sys::GhosttyRenderStateRowData::GHOSTTY_RENDER_STATE_ROW_DATA_CELLS,
&mut self.handle as *mut _ as *mut _,
)
};
check(r)?;
Ok(CellIter {
handle: self.handle,
_marker: PhantomData,
})
}
}
impl Drop for CellIterator {
fn drop(&mut self) {
unsafe {
sys::ghostty_render_state_row_cells_free(self.handle);
}
}
}
pub struct CellIter<'i> {
handle: sys::GhosttyRenderStateRowCells,
_marker: PhantomData<&'i mut ()>,
}
impl<'i> CellIter<'i> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<Cell<'_>> {
let ok = unsafe { sys::ghostty_render_state_row_cells_next(self.handle) };
if ok {
Some(Cell {
handle: self.handle,
_marker: PhantomData,
})
} else {
None
}
}
}
pub struct Cell<'i> {
handle: sys::GhosttyRenderStateRowCells,
_marker: PhantomData<&'i mut ()>,
}
impl<'i> Cell<'i> {
pub fn raw_cell(&self) -> Result<RawCell, Error> {
let mut out = MaybeUninit::<sys::GhosttyCell>::uninit();
let r = unsafe {
sys::ghostty_render_state_row_cells_get(
self.handle,
sys::GhosttyRenderStateRowCellsData::GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW,
out.as_mut_ptr().cast(),
)
};
check(r)?;
Ok(RawCell(unsafe { out.assume_init() }))
}
pub fn style(&self) -> Result<Style, Error> {
let mut out = Style::default_sys();
let r = unsafe {
sys::ghostty_render_state_row_cells_get(
self.handle,
sys::GhosttyRenderStateRowCellsData::GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
&mut out as *mut _ as *mut _,
)
};
check(r)?;
Ok(out.into())
}
pub fn fg_color(&self) -> Result<Option<RgbColor>, Error> {
let mut out = MaybeUninit::<sys::GhosttyColorRgb>::uninit();
let r = unsafe {
sys::ghostty_render_state_row_cells_get(
self.handle,
sys::GhosttyRenderStateRowCellsData::GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR,
out.as_mut_ptr().cast(),
)
};
match r {
sys::GhosttyResult::GHOSTTY_SUCCESS => Ok(Some(unsafe { out.assume_init() }.into())),
sys::GhosttyResult::GHOSTTY_INVALID_VALUE => Ok(None),
other => Err(other.into()),
}
}
pub fn bg_color(&self) -> Result<Option<RgbColor>, Error> {
let mut out = MaybeUninit::<sys::GhosttyColorRgb>::uninit();
let r = unsafe {
sys::ghostty_render_state_row_cells_get(
self.handle,
sys::GhosttyRenderStateRowCellsData::GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR,
out.as_mut_ptr().cast(),
)
};
match r {
sys::GhosttyResult::GHOSTTY_SUCCESS => Ok(Some(unsafe { out.assume_init() }.into())),
sys::GhosttyResult::GHOSTTY_INVALID_VALUE => Ok(None),
other => Err(other.into()),
}
}
pub fn graphemes(&self) -> Result<Vec<char>, Error> {
let mut len = MaybeUninit::<u32>::uninit();
let r = unsafe {
sys::ghostty_render_state_row_cells_get(
self.handle,
sys::GhosttyRenderStateRowCellsData::GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN,
len.as_mut_ptr().cast(),
)
};
check(r)?;
let len = unsafe { len.assume_init() } as usize;
if len == 0 {
return Ok(Vec::new());
}
let mut buf: Vec<u32> = vec![0u32; len];
let r = unsafe {
sys::ghostty_render_state_row_cells_get(
self.handle,
sys::GhosttyRenderStateRowCellsData::GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF,
buf.as_mut_ptr().cast(),
)
};
check(r)?;
Ok(buf.into_iter().filter_map(char::from_u32).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal::{Terminal, TerminalOptions};
fn read_cells(rows: u16, cols: u16, chunks: &[&[u8]], count: usize) -> Vec<Vec<char>> {
let mut term = Terminal::new(TerminalOptions {
cols,
rows,
max_scrollback: 0,
})
.unwrap();
for c in chunks {
term.vt_write(c).unwrap();
}
let mut rs = RenderState::new().unwrap();
let snapshot = rs.update(&term).unwrap();
let mut rows_h = RowIterator::new().unwrap();
let mut cells_h = CellIterator::new().unwrap();
let mut row_iter = rows_h.update(&snapshot).unwrap();
let mut out = Vec::with_capacity(count);
while let Some(row) = row_iter.next() {
let mut cell_iter = cells_h.update(&row).unwrap();
while let Some(cell) = cell_iter.next() {
if out.len() >= count {
return out;
}
out.push(cell.graphemes().unwrap());
}
}
out
}
#[test]
fn graphemes_reads_ascii() {
let cells = read_cells(4, 10, &[b"hello"], 5);
assert_eq!(cells[0], vec!['h']);
assert_eq!(cells[1], vec!['e']);
assert_eq!(cells[2], vec!['l']);
assert_eq!(cells[3], vec!['l']);
assert_eq!(cells[4], vec!['o']);
}
#[test]
fn graphemes_reads_combining_mark() {
let cells = read_cells(4, 10, &[b"he\xCC\x81llo"], 5);
assert_eq!(cells[0], vec!['h']);
assert_eq!(cells[1], vec!['e', '\u{301}']);
assert_eq!(cells[2], vec!['l']);
}
#[test]
fn graphemes_empty_cell_returns_empty() {
let cells = read_cells(4, 10, &[b"hi"], 5);
assert_eq!(cells[0], vec!['h']);
assert_eq!(cells[1], vec!['i']);
assert_eq!(cells[2], Vec::<char>::new());
assert_eq!(cells[3], Vec::<char>::new());
assert_eq!(cells[4], Vec::<char>::new());
}
}