aboutsummaryrefslogtreecommitdiff
path: root/src/ghostty/mod.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-02 02:27:14 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-02 02:31:12 +0300
commitcc527d45fe4821cb11c484372a9f25c51c02e078 (patch)
tree2e3411b39576b0910eba75e2b5b77c5e4e398e74 /src/ghostty/mod.rs
parentf7d315d494b6a3d583574012526d15c1ddc82bfe (diff)
perf: retain terminal render updates
Diffstat (limited to 'src/ghostty/mod.rs')
-rw-r--r--src/ghostty/mod.rs211
1 files changed, 211 insertions, 0 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
index 57114de4..d0700ef4 100644
--- a/src/ghostty/mod.rs
+++ b/src/ghostty/mod.rs
@@ -17,6 +17,7 @@ use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::mem;
+use std::ops::RangeInclusive;
use std::os::raw::c_char;
use std::ptr;
use std::slice;
@@ -71,6 +72,18 @@ pub enum Dirty {
Full,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct RowSelection {
+ pub start_x: u16,
+ pub end_x: u16,
+}
+
+impl RowSelection {
+ pub fn range(self) -> RangeInclusive<u16> {
+ self.start_x..=self.end_x
+ }
+}
+
impl Dirty {
fn from_raw(value: ffi::GhosttyRenderStateDirty) -> Self {
match value {
@@ -2186,6 +2199,44 @@ impl<'a> RowIter<'a> {
Ok(dirty)
}
+ pub fn clear_dirty(&mut self) -> Result<(), Error> {
+ self.set_dirty(false)
+ }
+
+ pub fn set_dirty(&mut self, dirty: bool) -> Result<(), Error> {
+ // SAFETY: dirty pointer matches the expected row option type.
+ unsafe {
+ ffi::ghostty_render_state_row_set(
+ self.iterator.raw,
+ ffi::GhosttyRenderStateRowOption_GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY,
+ (&dirty as *const bool).cast(),
+ )
+ .into_result()
+ }
+ }
+
+ pub fn selection(&self) -> Result<Option<RowSelection>, Error> {
+ let mut selection = ffi::GhosttyRenderStateRowSelection {
+ size: mem::size_of::<ffi::GhosttyRenderStateRowSelection>(),
+ ..Default::default()
+ };
+ let result = unsafe {
+ ffi::ghostty_render_state_row_get(
+ self.iterator.raw,
+ ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION,
+ (&mut selection as *mut ffi::GhosttyRenderStateRowSelection).cast(),
+ )
+ };
+ match result {
+ ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(RowSelection {
+ start_x: selection.start_x,
+ end_x: selection.end_x,
+ })),
+ ffi::GhosttyResult_GHOSTTY_NO_VALUE => Ok(None),
+ other => Err(Error(other)),
+ }
+ }
+
pub fn populate_cells<'b>(
&'b mut self,
cells: &'b mut RowCells,
@@ -2234,6 +2285,25 @@ pub struct RowCellIter<'a> {
cells: &'a mut RowCells,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CellBasicData {
+ pub wide: CellWide,
+ pub has_hyperlink: bool,
+ pub has_styling: bool,
+ pub style: CellStyle,
+}
+
+impl Default for CellBasicData {
+ fn default() -> Self {
+ Self {
+ wide: CellWide::Narrow,
+ has_hyperlink: false,
+ has_styling: false,
+ style: CellStyle::default(),
+ }
+ }
+}
+
impl<'a> RowCellIter<'a> {
pub fn next(&mut self) -> bool {
// SAFETY: cells handle is valid while self is alive.
@@ -2257,6 +2327,64 @@ impl<'a> RowCellIter<'a> {
Ok(raw)
}
+ pub fn basic_data(&self) -> Result<CellBasicData, Error> {
+ let mut raw = ffi::GhosttyCell::default();
+ let mut style = ffi::GhosttyStyle {
+ size: mem::size_of::<ffi::GhosttyStyle>(),
+ ..Default::default()
+ };
+ let mut has_styling = false;
+ let row_keys = [
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING,
+ ];
+ let mut row_values = [
+ (&mut raw as *mut ffi::GhosttyCell).cast::<c_void>(),
+ (&mut style as *mut ffi::GhosttyStyle).cast::<c_void>(),
+ (&mut has_styling as *mut bool).cast::<c_void>(),
+ ];
+ let mut written = 0usize;
+ unsafe {
+ ffi::ghostty_render_state_row_cells_get_multi(
+ self.cells.raw,
+ row_keys.len(),
+ row_keys.as_ptr(),
+ row_values.as_mut_ptr(),
+ &mut written,
+ )
+ .into_result()?;
+ }
+
+ let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW;
+ let mut has_hyperlink = false;
+ let cell_keys = [
+ ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE,
+ ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_HYPERLINK,
+ ];
+ let mut cell_values = [
+ (&mut wide as *mut ffi::GhosttyCellWide).cast::<c_void>(),
+ (&mut has_hyperlink as *mut bool).cast::<c_void>(),
+ ];
+ unsafe {
+ ffi::ghostty_cell_get_multi(
+ raw,
+ cell_keys.len(),
+ cell_keys.as_ptr(),
+ cell_values.as_mut_ptr(),
+ &mut written,
+ )
+ .into_result()?;
+ }
+
+ Ok(CellBasicData {
+ wide: CellWide::from_raw(wide),
+ has_hyperlink,
+ has_styling,
+ style: style.into(),
+ })
+ }
+
pub fn wide(&self) -> Result<CellWide, Error> {
let raw = self.raw_cell()?;
let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW;
@@ -2807,4 +2935,87 @@ mod tests {
render_state.set_dirty(Dirty::Clean).unwrap();
assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
}
+
+ #[test]
+ fn render_state_row_dirty_can_be_cleared_independently() {
+ let mut terminal = Terminal::new(8, 3, 100).unwrap();
+ let mut render_state = RenderState::new().unwrap();
+
+ render_state.update(&terminal).unwrap();
+ {
+ let mut row_iterator = RowIterator::new().unwrap();
+ let mut rows = render_state
+ .populate_row_iterator(&mut row_iterator)
+ .unwrap();
+ while rows.next() {
+ rows.clear_dirty().unwrap();
+ assert!(!rows.dirty().unwrap());
+ }
+ }
+ render_state.set_dirty(Dirty::Clean).unwrap();
+ assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
+
+ terminal.write(b"A");
+ render_state.update(&terminal).unwrap();
+ assert_eq!(render_state.dirty().unwrap(), Dirty::Partial);
+
+ let mut dirty_rows = 0usize;
+ {
+ let mut row_iterator = RowIterator::new().unwrap();
+ let mut rows = render_state
+ .populate_row_iterator(&mut row_iterator)
+ .unwrap();
+ while rows.next() {
+ if rows.dirty().unwrap() {
+ dirty_rows += 1;
+ rows.clear_dirty().unwrap();
+ assert!(!rows.dirty().unwrap());
+ }
+ }
+ }
+ assert_eq!(dirty_rows, 1);
+ assert_eq!(render_state.dirty().unwrap(), Dirty::Partial);
+
+ render_state.set_dirty(Dirty::Clean).unwrap();
+ assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
+ }
+
+ #[test]
+ fn row_selection_returns_none_without_selection() {
+ let terminal = Terminal::new(8, 3, 100).unwrap();
+ let mut render_state = RenderState::new().unwrap();
+ render_state.update(&terminal).unwrap();
+
+ let mut row_iterator = RowIterator::new().unwrap();
+ let mut rows = render_state
+ .populate_row_iterator(&mut row_iterator)
+ .unwrap();
+ assert!(rows.next());
+ assert_eq!(rows.selection().unwrap(), None);
+ }
+
+ #[test]
+ fn row_cell_basic_data_uses_batched_vendor_reads() {
+ let mut terminal = Terminal::new(8, 3, 100).unwrap();
+ terminal.write(b"\x1b[31mA\x1b[0m");
+
+ let mut render_state = RenderState::new().unwrap();
+ render_state.update(&terminal).unwrap();
+
+ let mut row_iterator = RowIterator::new().unwrap();
+ let mut rows = render_state
+ .populate_row_iterator(&mut row_iterator)
+ .unwrap();
+ assert!(rows.next());
+
+ let mut row_cells = RowCells::new().unwrap();
+ let mut cells = rows.populate_cells(&mut row_cells).unwrap();
+ assert!(cells.next());
+
+ let basic = cells.basic_data().unwrap();
+ assert_eq!(basic.wide, CellWide::Narrow);
+ assert!(basic.has_styling);
+ assert_eq!(basic.style.fg_color, Some(CellColor::Palette(1)));
+ assert!(!basic.has_hyperlink);
+ }
}