diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-02 02:27:14 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-02 02:31:12 +0300 |
| commit | cc527d45fe4821cb11c484372a9f25c51c02e078 (patch) | |
| tree | 2e3411b39576b0910eba75e2b5b77c5e4e398e74 /src/ghostty | |
| parent | f7d315d494b6a3d583574012526d15c1ddc82bfe (diff) | |
perf: retain terminal render updates
Diffstat (limited to 'src/ghostty')
| -rw-r--r-- | src/ghostty/bindings.rs | 79 | ||||
| -rw-r--r-- | src/ghostty/mod.rs | 211 |
2 files changed, 290 insertions, 0 deletions
diff --git a/src/ghostty/bindings.rs b/src/ghostty/bindings.rs index 4676c327..aa99b150 100644 --- a/src/ghostty/bindings.rs +++ b/src/ghostty/bindings.rs @@ -86,6 +86,8 @@ pub const GhosttyResult_GHOSTTY_OUT_OF_MEMORY: GhosttyResult = -1; pub const GhosttyResult_GHOSTTY_INVALID_VALUE: GhosttyResult = -2; #[doc = " Operation failed because the provided buffer was too small"] pub const GhosttyResult_GHOSTTY_OUT_OF_SPACE: GhosttyResult = -3; +#[doc = " Operation completed but the requested value was not present"] +pub const GhosttyResult_GHOSTTY_NO_VALUE: GhosttyResult = -4; #[doc = " Result codes for libghostty-vt operations."] pub type GhosttyResult = ::std::os::raw::c_int; #[doc = " A borrowed byte string (pointer + length).\n\n The memory is not owned by this struct. The pointer is only valid\n for the lifetime documented by the API that produces or consumes it."] @@ -575,6 +577,16 @@ unsafe extern "C" { ) -> GhosttyResult; } unsafe extern "C" { + #[doc = " Get multiple data fields from a cell in a single call."] + pub fn ghostty_cell_get_multi( + cell: GhosttyCell, + count: usize, + keys: *const GhosttyCellData, + values: *mut *mut ::std::os::raw::c_void, + out_written: *mut usize, + ) -> GhosttyResult; +} +unsafe extern "C" { #[doc = " Get data from a row.\n\n Extracts typed data from the given row based on the specified\n data type. The output pointer must be of the appropriate type for the\n requested data kind. Valid data types and output types are documented\n in the `GhosttyRowData` enum.\n\n @param row The row value\n @param data The type of data to extract\n @param out Pointer to store the extracted data (type depends on data parameter)\n @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the\n data type is invalid\n\n @ingroup screen"] pub fn ghostty_row_get( row: GhosttyRow, @@ -582,6 +594,16 @@ unsafe extern "C" { out: *mut ::std::os::raw::c_void, ) -> GhosttyResult; } +unsafe extern "C" { + #[doc = " Get multiple data fields from a row in a single call."] + pub fn ghostty_row_get_multi( + row: GhosttyRow, + count: usize, + keys: *const GhosttyRowData, + values: *mut *mut ::std::os::raw::c_void, + out_written: *mut usize, + ) -> GhosttyResult; +} #[doc = " Style identifier type.\n\n Used to look up the full style from a grid reference.\n Obtain this from a cell via GHOSTTY_CELL_DATA_STYLE_ID.\n\n @ingroup style"] pub type GhosttyStyleId = u16; pub const GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_NONE: GhosttyStyleColorTag = 0; @@ -1479,6 +1501,9 @@ pub const GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_RAW: GhosttyRe #[doc = " Populate a pre-allocated GhosttyRenderStateRowCells with cell data for\n the current row (GhosttyRenderStateRowCells). Cell data is only\n valid as long as the underlying render state is not updated.\n It is unsafe to use cell data after updating the render state."] pub const GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_CELLS: GhosttyRenderStateRowData = 3; +#[doc = " Row-local selected cell range (GhosttyRenderStateRowSelection)."] +pub const GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION: + GhosttyRenderStateRowData = 4; #[doc = " Queryable data kinds for ghostty_render_state_row_get().\n\n @ingroup render"] pub type GhosttyRenderStateRowData = ::std::os::raw::c_uint; #[doc = " Set dirty state for the current row (bool)."] @@ -1486,6 +1511,30 @@ pub const GhosttyRenderStateRowOption_GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY: GhosttyRenderStateRowOption = 0; #[doc = " Settable options for ghostty_render_state_row_set().\n\n @ingroup render"] pub type GhosttyRenderStateRowOption = ::std::os::raw::c_uint; +#[doc = " Row-local selection range."] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct GhosttyRenderStateRowSelection { + #[doc = " Size of this struct in bytes."] + pub size: usize, + #[doc = " Start column of the row-local selection range, inclusive."] + pub start_x: u16, + #[doc = " End column of the row-local selection range, inclusive."] + pub end_x: u16, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of GhosttyRenderStateRowSelection"] + [::std::mem::size_of::<GhosttyRenderStateRowSelection>() - 16usize]; + ["Alignment of GhosttyRenderStateRowSelection"] + [::std::mem::align_of::<GhosttyRenderStateRowSelection>() - 8usize]; + ["Offset of field: GhosttyRenderStateRowSelection::size"] + [::std::mem::offset_of!(GhosttyRenderStateRowSelection, size) - 0usize]; + ["Offset of field: GhosttyRenderStateRowSelection::start_x"] + [::std::mem::offset_of!(GhosttyRenderStateRowSelection, start_x) - 8usize]; + ["Offset of field: GhosttyRenderStateRowSelection::end_x"] + [::std::mem::offset_of!(GhosttyRenderStateRowSelection, end_x) - 10usize]; +}; #[doc = " Render-state color information.\n\n This struct uses the sized-struct ABI pattern. Initialize with\n GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling\n ghostty_render_state_colors_get().\n\n Example:\n @code\n GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);\n GhosttyResult result = ghostty_render_state_colors_get(state, &colors);\n @endcode\n\n @ingroup render"] #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -1558,6 +1607,16 @@ unsafe extern "C" { ) -> GhosttyResult; } unsafe extern "C" { + #[doc = " Get multiple data fields from a render state in a single call."] + pub fn ghostty_render_state_get_multi( + state: GhosttyRenderState_ptr, + count: usize, + keys: *const GhosttyRenderStateData, + values: *mut *mut ::std::os::raw::c_void, + out_written: *mut usize, + ) -> GhosttyResult; +} +unsafe extern "C" { #[doc = " Set an option on a render state.\n\n The `value` pointer must point to a value of the type corresponding to the\n requested option kind (see GhosttyRenderStateOption).\n\n @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)\n @param option The option to set\n @param[in] value Pointer to the value to set (NULL returns\n GHOSTTY_INVALID_VALUE)\n @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or\n `value` is NULL\n\n @ingroup render"] pub fn ghostty_render_state_set( state: GhosttyRenderState_ptr, @@ -1598,6 +1657,16 @@ unsafe extern "C" { ) -> GhosttyResult; } unsafe extern "C" { + #[doc = " Get multiple data fields from the current row in a single call."] + pub fn ghostty_render_state_row_get_multi( + iterator: GhosttyRenderStateRowIterator_ptr, + count: usize, + keys: *const GhosttyRenderStateRowData, + values: *mut *mut ::std::os::raw::c_void, + out_written: *mut usize, + ) -> GhosttyResult; +} +unsafe extern "C" { #[doc = " Set an option on the current row in a render-state row iterator.\n\n The `value` pointer must point to a value of the type corresponding to the\n requested option kind (see GhosttyRenderStateRowOption).\n Call ghostty_render_state_row_iterator_next() at least once before\n calling this function.\n\n @param iterator The iterator handle to update (NULL returns GHOSTTY_INVALID_VALUE)\n @param option The option to set\n @param[in] value Pointer to the value to set (NULL returns\n GHOSTTY_INVALID_VALUE)\n @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if\n `iterator` is NULL or the iterator is not positioned on a row\n\n @ingroup render"] pub fn ghostty_render_state_row_set( iterator: GhosttyRenderStateRowIterator_ptr, @@ -1664,6 +1733,16 @@ unsafe extern "C" { ) -> GhosttyResult; } unsafe extern "C" { + #[doc = " Get multiple data fields from the current cell in a single call."] + pub fn ghostty_render_state_row_cells_get_multi( + cells: GhosttyRenderStateRowCells_ptr, + count: usize, + keys: *const GhosttyRenderStateRowCellsData, + values: *mut *mut ::std::os::raw::c_void, + out_written: *mut usize, + ) -> GhosttyResult; +} +unsafe extern "C" { #[doc = " Free a row cells instance.\n\n @param cells The row cells handle to free (may be NULL)\n\n @ingroup render"] pub fn ghostty_render_state_row_cells_free(cells: GhosttyRenderStateRowCells_ptr); } 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); + } } |
