aboutsummaryrefslogtreecommitdiff
path: root/src/ghostty/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ghostty/mod.rs')
-rw-r--r--src/ghostty/mod.rs1106
1 files changed, 1106 insertions, 0 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
new file mode 100644
index 00000000..8c5e591d
--- /dev/null
+++ b/src/ghostty/mod.rs
@@ -0,0 +1,1106 @@
+#![cfg(feature = "ghostty-vt")]
+#![allow(dead_code)]
+
+#[allow(
+ dead_code,
+ non_camel_case_types,
+ non_snake_case,
+ non_upper_case_globals,
+ clippy::all,
+ rustdoc::all
+)]
+pub mod bindings;
+
+use std::ffi::c_void;
+use std::fmt;
+use std::marker::PhantomData;
+use std::mem;
+use std::os::raw::c_char;
+use std::ptr;
+use std::slice;
+
+pub use bindings as ffi;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Error(ffi::GhosttyResult);
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "ghostty error {}", self.0)
+ }
+}
+
+impl std::error::Error for Error {}
+
+trait GhosttyResultExt {
+ fn into_result(self) -> Result<(), Error>;
+}
+
+impl GhosttyResultExt for ffi::GhosttyResult {
+ fn into_result(self) -> Result<(), Error> {
+ if self == ffi::GhosttyResult_GHOSTTY_SUCCESS {
+ Ok(())
+ } else {
+ Err(Error(self))
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Dirty {
+ Clean,
+ Partial,
+ Full,
+}
+
+impl Dirty {
+ fn from_raw(value: ffi::GhosttyRenderStateDirty) -> Self {
+ match value {
+ ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE => Self::Clean,
+ ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_PARTIAL => Self::Partial,
+ ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FULL => Self::Full,
+ _ => Self::Full,
+ }
+ }
+
+ fn as_raw(self) -> ffi::GhosttyRenderStateDirty {
+ match self {
+ Self::Clean => ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE,
+ Self::Partial => ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_PARTIAL,
+ Self::Full => ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FULL,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FocusEvent {
+ Gained,
+ Lost,
+}
+
+impl FocusEvent {
+ fn as_raw(self) -> ffi::GhosttyFocusEvent {
+ match self {
+ Self::Gained => ffi::GhosttyFocusEvent_GHOSTTY_FOCUS_GAINED,
+ Self::Lost => ffi::GhosttyFocusEvent_GHOSTTY_FOCUS_LOST,
+ }
+ }
+}
+
+pub const MOD_SHIFT: u16 = ffi::GHOSTTY_MODS_SHIFT as u16;
+pub const MOD_CTRL: u16 = ffi::GHOSTTY_MODS_CTRL as u16;
+pub const MOD_ALT: u16 = ffi::GHOSTTY_MODS_ALT as u16;
+pub const MOD_SUPER: u16 = ffi::GHOSTTY_MODS_SUPER as u16;
+
+pub const KEY_ENTER: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ENTER;
+pub const KEY_UP: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_UP;
+pub const KEY_DOWN: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_DOWN;
+pub const KEY_LEFT: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_LEFT;
+pub const KEY_RIGHT: u32 = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_RIGHT;
+pub const KEY_A: u32 = ffi::GhosttyKey_GHOSTTY_KEY_A;
+
+pub const MOUSE_ACTION_PRESS: ffi::GhosttyMouseAction =
+ ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_PRESS;
+pub const MOUSE_ACTION_RELEASE: ffi::GhosttyMouseAction =
+ ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_RELEASE;
+pub const MOUSE_ACTION_MOTION: ffi::GhosttyMouseAction =
+ ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_MOTION;
+pub const MOUSE_BUTTON_LEFT: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_LEFT;
+pub const MOUSE_BUTTON_RIGHT: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_RIGHT;
+pub const MOUSE_BUTTON_MIDDLE: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_MIDDLE;
+pub const MOUSE_BUTTON_WHEEL_UP: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_FOUR;
+pub const MOUSE_BUTTON_WHEEL_DOWN: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_FIVE;
+pub const MOUSE_BUTTON_WHEEL_LEFT: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SIX;
+pub const MOUSE_BUTTON_WHEEL_RIGHT: ffi::GhosttyMouseButton =
+ ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SEVEN;
+
+pub const MODE_APPLICATION_CURSOR_KEYS: u16 = 1;
+pub const MODE_BRACKETED_PASTE: u16 = 2004;
+pub const MODE_MOUSE_UTF8: u16 = 1005;
+pub const MODE_MOUSE_SGR: u16 = 1006;
+pub const MODE_MOUSE_ALTERNATE_SCROLL: u16 = 1007;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ActiveScreen {
+ Primary,
+ Alternate,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct TerminalScrollbar {
+ pub total: usize,
+ pub offset: usize,
+ pub len: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct CursorViewport {
+ pub x: u16,
+ pub y: u16,
+ pub wide_tail: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct RgbColor {
+ pub r: u8,
+ pub g: u8,
+ pub b: u8,
+}
+
+impl From<ffi::GhosttyColorRgb> for RgbColor {
+ fn from(value: ffi::GhosttyColorRgb) -> Self {
+ Self {
+ r: value.r,
+ g: value.g,
+ b: value.b,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct CellStyle {
+ pub bold: bool,
+ pub italic: bool,
+ pub faint: bool,
+ pub blink: bool,
+ pub inverse: bool,
+ pub invisible: bool,
+ pub strikethrough: bool,
+ pub overline: bool,
+ pub underlined: bool,
+}
+
+impl From<ffi::GhosttyStyle> for CellStyle {
+ fn from(value: ffi::GhosttyStyle) -> Self {
+ Self {
+ bold: value.bold,
+ italic: value.italic,
+ faint: value.faint,
+ blink: value.blink,
+ inverse: value.inverse,
+ invisible: value.invisible,
+ strikethrough: value.strikethrough,
+ overline: value.overline,
+ underlined: value.underline != 0,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct RenderColors {
+ pub background: RgbColor,
+ pub foreground: RgbColor,
+}
+
+struct WritePtyCallbackState {
+ callback: Box<dyn FnMut(&[u8]) + Send>,
+}
+
+unsafe extern "C" fn write_pty_trampoline(
+ _terminal: ffi::GhosttyTerminal_ptr,
+ userdata: *mut c_void,
+ data: *const u8,
+ len: usize,
+) {
+ if userdata.is_null() {
+ return;
+ }
+ let state = unsafe { &mut *(userdata.cast::<WritePtyCallbackState>()) };
+ let bytes = unsafe { slice::from_raw_parts(data, len) };
+ (state.callback)(bytes);
+}
+
+pub fn encode_focus(event: FocusEvent) -> Result<Vec<u8>, Error> {
+ let mut required = 0usize;
+ // SAFETY: null buffer + out len is the documented way to query required size.
+ let result =
+ unsafe { ffi::ghostty_focus_encode(event.as_raw(), ptr::null_mut(), 0, &mut required) };
+ if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
+ result.into_result()?;
+ }
+
+ let mut buffer = vec![0u8; required];
+ // SAFETY: buffer is allocated for required size; function writes at most that many bytes.
+ unsafe {
+ ffi::ghostty_focus_encode(
+ event.as_raw(),
+ buffer.as_mut_ptr().cast(),
+ buffer.len(),
+ &mut required,
+ )
+ .into_result()?;
+ }
+ buffer.truncate(required);
+ Ok(buffer)
+}
+
+pub struct Terminal {
+ raw: ffi::GhosttyTerminal_ptr,
+ write_pty_callback: Option<Box<WritePtyCallbackState>>,
+}
+
+impl Terminal {
+ pub fn new(cols: u16, rows: u16, max_scrollback: usize) -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ let options = ffi::GhosttyTerminalOptions {
+ cols,
+ rows,
+ max_scrollback,
+ };
+ // SAFETY: valid out pointer and options, null allocator means default allocator.
+ unsafe {
+ ffi::ghostty_terminal_new(ptr::null(), &mut raw, options).into_result()?;
+ }
+ Ok(Self {
+ raw,
+ write_pty_callback: None,
+ })
+ }
+
+ pub fn write(&mut self, bytes: &[u8]) {
+ // SAFETY: self.raw is a live terminal handle for self's lifetime.
+ unsafe {
+ ffi::ghostty_terminal_vt_write(self.raw, bytes.as_ptr(), bytes.len());
+ }
+ }
+
+ pub fn resize(&mut self, cols: u16, rows: u16) -> Result<(), Error> {
+ // SAFETY: self.raw is valid and sizes are plain values.
+ unsafe { ffi::ghostty_terminal_resize(self.raw, cols, rows, 0, 0).into_result() }
+ }
+
+ pub fn set_write_pty_callback<F>(&mut self, callback: F) -> Result<(), Error>
+ where
+ F: FnMut(&[u8]) + Send + 'static,
+ {
+ let mut state = Box::new(WritePtyCallbackState {
+ callback: Box::new(callback),
+ });
+ let userdata = (&mut *state as *mut WritePtyCallbackState).cast::<c_void>();
+ unsafe {
+ ffi::ghostty_terminal_set(
+ self.raw,
+ ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_USERDATA,
+ userdata.cast(),
+ )
+ .into_result()?;
+ ffi::ghostty_terminal_set(
+ self.raw,
+ ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_WRITE_PTY,
+ (write_pty_trampoline as *const ()).cast(),
+ )
+ .into_result()?;
+ }
+ self.write_pty_callback = Some(state);
+ Ok(())
+ }
+
+ pub fn mode_get(&self, mode: u16) -> Result<bool, Error> {
+ let mut out = false;
+ unsafe { ffi::ghostty_terminal_mode_get(self.raw, mode, &mut out).into_result()? };
+ Ok(out)
+ }
+
+ pub fn mode_set(&mut self, mode: u16, value: bool) -> Result<(), Error> {
+ unsafe { ffi::ghostty_terminal_mode_set(self.raw, mode, value).into_result() }
+ }
+
+ pub fn kitty_keyboard_flags(&self) -> Result<u8, Error> {
+ let mut out = 0u8;
+ unsafe {
+ ffi::ghostty_terminal_get(
+ self.raw,
+ ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS,
+ (&mut out as *mut u8).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(out)
+ }
+
+ pub fn mouse_tracking_enabled(&self) -> Result<bool, Error> {
+ let mut out = false;
+ unsafe {
+ ffi::ghostty_terminal_get(
+ self.raw,
+ ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING,
+ (&mut out as *mut bool).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(out)
+ }
+
+ pub fn active_screen(&self) -> Result<ActiveScreen, Error> {
+ let mut out = ffi::GhosttyTerminalScreen_GHOSTTY_TERMINAL_SCREEN_PRIMARY;
+ unsafe {
+ ffi::ghostty_terminal_get(
+ self.raw,
+ ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN,
+ (&mut out as *mut ffi::GhosttyTerminalScreen).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(match out {
+ ffi::GhosttyTerminalScreen_GHOSTTY_TERMINAL_SCREEN_PRIMARY => ActiveScreen::Primary,
+ ffi::GhosttyTerminalScreen_GHOSTTY_TERMINAL_SCREEN_ALTERNATE => {
+ ActiveScreen::Alternate
+ }
+ _ => ActiveScreen::Primary,
+ })
+ }
+
+ pub fn total_rows(&self) -> Result<usize, Error> {
+ self.get_usize(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_TOTAL_ROWS)
+ }
+
+ pub fn scrollback_rows(&self) -> Result<usize, Error> {
+ self.get_usize(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS)
+ }
+
+ pub fn scrollbar(&self) -> Result<TerminalScrollbar, Error> {
+ let mut out = ffi::GhosttyTerminalScrollbar::default();
+ unsafe {
+ ffi::ghostty_terminal_get(
+ self.raw,
+ ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_SCROLLBAR,
+ (&mut out as *mut ffi::GhosttyTerminalScrollbar).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(TerminalScrollbar {
+ total: out.total as usize,
+ offset: out.offset as usize,
+ len: out.len as usize,
+ })
+ }
+
+ pub fn screen_graphemes(&self, x: u16, y: u32) -> Result<Vec<u32>, Error> {
+ let point = ffi::GhosttyPoint {
+ tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN,
+ value: ffi::GhosttyPointValue {
+ coordinate: ffi::GhosttyPointCoordinate { x, y },
+ },
+ };
+ let mut grid_ref = ffi::GhosttyGridRef {
+ size: mem::size_of::<ffi::GhosttyGridRef>(),
+ ..Default::default()
+ };
+ unsafe {
+ ffi::ghostty_terminal_grid_ref(self.raw, point, &mut grid_ref).into_result()?;
+ }
+ let mut required = 0usize;
+ let result = unsafe {
+ ffi::ghostty_grid_ref_graphemes(&grid_ref, ptr::null_mut(), 0, &mut required)
+ };
+ if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
+ result.into_result()?;
+ }
+ let mut buffer = vec![0u32; required];
+ if required == 0 {
+ return Ok(buffer);
+ }
+ unsafe {
+ ffi::ghostty_grid_ref_graphemes(&grid_ref, buffer.as_mut_ptr(), buffer.len(), &mut required)
+ .into_result()?;
+ }
+ buffer.truncate(required);
+ Ok(buffer)
+ }
+
+ pub fn scroll_viewport_bottom(&mut self) {
+ let viewport = ffi::GhosttyTerminalScrollViewport {
+ tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_BOTTOM,
+ value: ffi::GhosttyTerminalScrollViewportValue::default(),
+ };
+ // SAFETY: self.raw is valid and viewport value matches the tag.
+ unsafe {
+ ffi::ghostty_terminal_scroll_viewport(self.raw, viewport);
+ }
+ }
+
+ pub fn scroll_viewport_delta(&mut self, delta: isize) {
+ let viewport = ffi::GhosttyTerminalScrollViewport {
+ tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_DELTA,
+ value: ffi::GhosttyTerminalScrollViewportValue { delta },
+ };
+ // SAFETY: self.raw is valid and viewport value matches the tag.
+ unsafe {
+ ffi::ghostty_terminal_scroll_viewport(self.raw, viewport);
+ }
+ }
+
+ pub fn cols(&self) -> Result<u16, Error> {
+ self.get_u16(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_COLS)
+ }
+
+ pub fn rows(&self) -> Result<u16, Error> {
+ self.get_u16(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ROWS)
+ }
+
+ fn get_u16(&self, data: ffi::GhosttyTerminalData) -> Result<u16, Error> {
+ let mut out = 0u16;
+ // SAFETY: out points to a u16 matching the requested terminal data type.
+ unsafe {
+ ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut u16).cast())
+ .into_result()?;
+ }
+ Ok(out)
+ }
+
+ fn get_usize(&self, data: ffi::GhosttyTerminalData) -> Result<usize, Error> {
+ let mut out = 0usize;
+ unsafe {
+ ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut usize).cast())
+ .into_result()?;
+ }
+ Ok(out)
+ }
+
+ fn raw(&self) -> ffi::GhosttyTerminal_ptr {
+ self.raw
+ }
+}
+
+// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
+unsafe impl Send for Terminal {}
+
+impl Drop for Terminal {
+ fn drop(&mut self) {
+ // SAFETY: freeing a null or live handle is allowed by the C API.
+ unsafe {
+ ffi::ghostty_terminal_free(self.raw);
+ }
+ }
+}
+
+pub struct RenderState {
+ raw: ffi::GhosttyRenderState_ptr,
+}
+
+impl RenderState {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ // SAFETY: valid out pointer and null allocator use default allocator.
+ unsafe {
+ ffi::ghostty_render_state_new(ptr::null(), &mut raw).into_result()?;
+ }
+ Ok(Self { raw })
+ }
+
+ pub fn update(&mut self, terminal: &Terminal) -> Result<(), Error> {
+ // SAFETY: both handles are valid for the duration of the call.
+ unsafe { ffi::ghostty_render_state_update(self.raw, terminal.raw()).into_result() }
+ }
+
+ pub fn cols(&self) -> Result<u16, Error> {
+ self.get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLS)
+ }
+
+ pub fn rows(&self) -> Result<u16, Error> {
+ self.get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROWS)
+ }
+
+ pub fn dirty(&self) -> Result<Dirty, Error> {
+ let mut out = ffi::GhosttyRenderStateDirty_GHOSTTY_RENDER_STATE_DIRTY_FALSE;
+ // SAFETY: out points to the matching enum storage for the requested data kind.
+ unsafe {
+ ffi::ghostty_render_state_get(
+ self.raw,
+ ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_DIRTY,
+ (&mut out as *mut ffi::GhosttyRenderStateDirty).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(Dirty::from_raw(out))
+ }
+
+ pub fn cursor_visible(&self) -> Result<bool, Error> {
+ self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE)
+ }
+
+ pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>, Error> {
+ if !self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE)? {
+ return Ok(None);
+ }
+ Ok(Some(CursorViewport {
+ x: self.get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X)?,
+ y: self.get_u16(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y)?,
+ wide_tail: self.get_bool(
+ ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
+ )?,
+ }))
+ }
+
+ pub fn colors(&self) -> Result<RenderColors, Error> {
+ let mut colors = ffi::GhosttyRenderStateColors {
+ size: mem::size_of::<ffi::GhosttyRenderStateColors>(),
+ ..Default::default()
+ };
+ unsafe {
+ ffi::ghostty_render_state_colors_get(self.raw, &mut colors).into_result()?;
+ }
+ Ok(RenderColors {
+ background: colors.background.into(),
+ foreground: colors.foreground.into(),
+ })
+ }
+
+ pub fn set_dirty(&mut self, dirty: Dirty) -> Result<(), Error> {
+ let value = dirty.as_raw();
+ // SAFETY: value pointer matches the expected option type.
+ unsafe {
+ ffi::ghostty_render_state_set(
+ self.raw,
+ ffi::GhosttyRenderStateOption_GHOSTTY_RENDER_STATE_OPTION_DIRTY,
+ (&value as *const ffi::GhosttyRenderStateDirty).cast(),
+ )
+ .into_result()
+ }
+ }
+
+ pub fn populate_row_iterator<'a>(
+ &'a self,
+ iterator: &'a mut RowIterator,
+ ) -> Result<RowIter<'a>, Error> {
+ // SAFETY: iterator raw handle is valid and will not outlive self.
+ unsafe {
+ ffi::ghostty_render_state_get(
+ self.raw,
+ ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR,
+ (&mut iterator.raw as *mut ffi::GhosttyRenderStateRowIterator_ptr).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(RowIter {
+ iterator,
+ _state: PhantomData,
+ })
+ }
+
+ fn get_u16(&self, data: ffi::GhosttyRenderStateData) -> Result<u16, Error> {
+ let mut out = 0u16;
+ // SAFETY: out points to a u16 matching the requested render-state data type.
+ unsafe {
+ ffi::ghostty_render_state_get(self.raw, data, (&mut out as *mut u16).cast())
+ .into_result()?;
+ }
+ Ok(out)
+ }
+
+ fn get_bool(&self, data: ffi::GhosttyRenderStateData) -> Result<bool, Error> {
+ let mut out = false;
+ unsafe {
+ ffi::ghostty_render_state_get(self.raw, data, (&mut out as *mut bool).cast())
+ .into_result()?;
+ }
+ Ok(out)
+ }
+}
+
+// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
+unsafe impl Send for RenderState {}
+
+impl Drop for RenderState {
+ fn drop(&mut self) {
+ // SAFETY: freeing a null or live handle is allowed by the C API.
+ unsafe {
+ ffi::ghostty_render_state_free(self.raw);
+ }
+ }
+}
+
+pub struct KeyEvent {
+ raw: ffi::GhosttyKeyEvent_ptr,
+}
+
+impl KeyEvent {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ unsafe { ffi::ghostty_key_event_new(ptr::null(), &mut raw).into_result()? };
+ Ok(Self { raw })
+ }
+
+ pub fn set_action(&mut self, action: ffi::GhosttyKeyAction) {
+ unsafe { ffi::ghostty_key_event_set_action(self.raw, action) }
+ }
+
+ pub fn set_key(&mut self, key: u32) {
+ unsafe { ffi::ghostty_key_event_set_key(self.raw, key) }
+ }
+
+ pub fn set_mods(&mut self, mods: u16) {
+ unsafe { ffi::ghostty_key_event_set_mods(self.raw, mods) }
+ }
+
+ pub fn set_utf8(&mut self, text: &str) {
+ unsafe {
+ ffi::ghostty_key_event_set_utf8(self.raw, text.as_ptr().cast::<c_char>(), text.len())
+ }
+ }
+
+ pub fn set_unshifted_codepoint(&mut self, codepoint: u32) {
+ unsafe { ffi::ghostty_key_event_set_unshifted_codepoint(self.raw, codepoint) }
+ }
+}
+
+impl Drop for KeyEvent {
+ fn drop(&mut self) {
+ unsafe { ffi::ghostty_key_event_free(self.raw) }
+ }
+}
+
+pub struct KeyEncoder {
+ raw: ffi::GhosttyKeyEncoder_ptr,
+}
+
+impl KeyEncoder {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ unsafe { ffi::ghostty_key_encoder_new(ptr::null(), &mut raw).into_result()? };
+ Ok(Self { raw })
+ }
+
+ pub fn set_from_terminal(&mut self, terminal: &Terminal) {
+ unsafe { ffi::ghostty_key_encoder_setopt_from_terminal(self.raw, terminal.raw()) }
+ }
+
+ pub fn encode(&mut self, event: &KeyEvent) -> Result<Vec<u8>, Error> {
+ encode_with_retry(|buf, len, out_len| unsafe {
+ ffi::ghostty_key_encoder_encode(self.raw, event.raw, buf, len, out_len)
+ })
+ }
+}
+
+impl Drop for KeyEncoder {
+ fn drop(&mut self) {
+ unsafe { ffi::ghostty_key_encoder_free(self.raw) }
+ }
+}
+
+pub struct MouseEvent {
+ raw: ffi::GhosttyMouseEvent_ptr,
+}
+
+impl MouseEvent {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ unsafe { ffi::ghostty_mouse_event_new(ptr::null(), &mut raw).into_result()? };
+ Ok(Self { raw })
+ }
+
+ pub fn set_action(&mut self, action: ffi::GhosttyMouseAction) {
+ unsafe { ffi::ghostty_mouse_event_set_action(self.raw, action) }
+ }
+
+ pub fn set_button(&mut self, button: ffi::GhosttyMouseButton) {
+ unsafe { ffi::ghostty_mouse_event_set_button(self.raw, button) }
+ }
+
+ pub fn clear_button(&mut self) {
+ unsafe { ffi::ghostty_mouse_event_clear_button(self.raw) }
+ }
+
+ pub fn set_mods(&mut self, mods: u16) {
+ unsafe { ffi::ghostty_mouse_event_set_mods(self.raw, mods) }
+ }
+
+ pub fn set_position(&mut self, x: f32, y: f32) {
+ unsafe {
+ ffi::ghostty_mouse_event_set_position(self.raw, ffi::GhosttyMousePosition { x, y })
+ }
+ }
+}
+
+impl Drop for MouseEvent {
+ fn drop(&mut self) {
+ unsafe { ffi::ghostty_mouse_event_free(self.raw) }
+ }
+}
+
+pub struct MouseEncoder {
+ raw: ffi::GhosttyMouseEncoder_ptr,
+}
+
+impl MouseEncoder {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ unsafe { ffi::ghostty_mouse_encoder_new(ptr::null(), &mut raw).into_result()? };
+ Ok(Self { raw })
+ }
+
+ pub fn set_from_terminal(&mut self, terminal: &Terminal) {
+ unsafe { ffi::ghostty_mouse_encoder_setopt_from_terminal(self.raw, terminal.raw()) }
+ }
+
+ pub fn set_size(
+ &mut self,
+ screen_width: u32,
+ screen_height: u32,
+ cell_width: u32,
+ cell_height: u32,
+ ) {
+ let size = ffi::GhosttyMouseEncoderSize {
+ size: std::mem::size_of::<ffi::GhosttyMouseEncoderSize>(),
+ screen_width,
+ screen_height,
+ cell_width,
+ cell_height,
+ padding_top: 0,
+ padding_bottom: 0,
+ padding_right: 0,
+ padding_left: 0,
+ };
+ unsafe {
+ ffi::ghostty_mouse_encoder_setopt(
+ self.raw,
+ ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_SIZE,
+ (&size as *const ffi::GhosttyMouseEncoderSize).cast(),
+ )
+ }
+ }
+
+ pub fn encode(&mut self, event: &MouseEvent) -> Result<Vec<u8>, Error> {
+ encode_with_retry(|buf, len, out_len| unsafe {
+ ffi::ghostty_mouse_encoder_encode(self.raw, event.raw, buf, len, out_len)
+ })
+ }
+}
+
+impl Drop for MouseEncoder {
+ fn drop(&mut self) {
+ unsafe { ffi::ghostty_mouse_encoder_free(self.raw) }
+ }
+}
+
+fn encode_with_retry(
+ mut encode: impl FnMut(*mut c_char, usize, *mut usize) -> ffi::GhosttyResult,
+) -> Result<Vec<u8>, Error> {
+ let mut required = 0usize;
+ let result = encode(ptr::null_mut(), 0, &mut required);
+ if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
+ result.into_result()?;
+ }
+ let mut buffer = vec![0u8; required.max(16)];
+ let mut written = 0usize;
+ encode(
+ buffer.as_mut_ptr().cast::<c_char>(),
+ buffer.len(),
+ &mut written,
+ )
+ .into_result()?;
+ buffer.truncate(written);
+ Ok(buffer)
+}
+
+pub struct RowIterator {
+ raw: ffi::GhosttyRenderStateRowIterator_ptr,
+}
+
+impl RowIterator {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ // SAFETY: valid out pointer and null allocator use default allocator.
+ unsafe {
+ ffi::ghostty_render_state_row_iterator_new(ptr::null(), &mut raw).into_result()?;
+ }
+ Ok(Self { raw })
+ }
+}
+
+// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
+unsafe impl Send for RowIterator {}
+
+impl Drop for RowIterator {
+ fn drop(&mut self) {
+ // SAFETY: freeing a null or live handle is allowed by the C API.
+ unsafe {
+ ffi::ghostty_render_state_row_iterator_free(self.raw);
+ }
+ }
+}
+
+pub struct RowIter<'a> {
+ iterator: &'a mut RowIterator,
+ _state: PhantomData<&'a RenderState>,
+}
+
+impl<'a> RowIter<'a> {
+ pub fn next(&mut self) -> bool {
+ // SAFETY: iterator handle is valid while self is alive.
+ unsafe { ffi::ghostty_render_state_row_iterator_next(self.iterator.raw) }
+ }
+
+ pub fn dirty(&self) -> Result<bool, Error> {
+ let mut dirty = false;
+ // SAFETY: dirty output matches requested row data type.
+ unsafe {
+ ffi::ghostty_render_state_row_get(
+ self.iterator.raw,
+ ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY,
+ (&mut dirty as *mut bool).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(dirty)
+ }
+
+ pub fn populate_cells<'b>(
+ &'b mut self,
+ cells: &'b mut RowCells,
+ ) -> Result<RowCellIter<'b>, Error> {
+ // SAFETY: cells raw handle is valid and will not outlive the current row borrow.
+ unsafe {
+ ffi::ghostty_render_state_row_get(
+ self.iterator.raw,
+ ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_CELLS,
+ (&mut cells.raw as *mut ffi::GhosttyRenderStateRowCells_ptr).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(RowCellIter { cells })
+ }
+}
+
+pub struct RowCells {
+ raw: ffi::GhosttyRenderStateRowCells_ptr,
+}
+
+impl RowCells {
+ pub fn new() -> Result<Self, Error> {
+ let mut raw = ptr::null_mut();
+ // SAFETY: valid out pointer and null allocator use default allocator.
+ unsafe {
+ ffi::ghostty_render_state_row_cells_new(ptr::null(), &mut raw).into_result()?;
+ }
+ Ok(Self { raw })
+ }
+}
+
+// SAFETY: these opaque handles are only used behind external synchronization in pane runtime.
+unsafe impl Send for RowCells {}
+
+impl Drop for RowCells {
+ fn drop(&mut self) {
+ // SAFETY: freeing a null or live handle is allowed by the C API.
+ unsafe {
+ ffi::ghostty_render_state_row_cells_free(self.raw);
+ }
+ }
+}
+
+pub struct RowCellIter<'a> {
+ cells: &'a mut RowCells,
+}
+
+impl<'a> RowCellIter<'a> {
+ pub fn next(&mut self) -> bool {
+ // SAFETY: cells handle is valid while self is alive.
+ unsafe { ffi::ghostty_render_state_row_cells_next(self.cells.raw) }
+ }
+
+ pub fn select(&mut self, x: u16) -> Result<(), Error> {
+ unsafe { ffi::ghostty_render_state_row_cells_select(self.cells.raw, x).into_result() }
+ }
+
+ pub fn style(&self) -> Result<CellStyle, Error> {
+ let mut style = ffi::GhosttyStyle {
+ size: mem::size_of::<ffi::GhosttyStyle>(),
+ ..Default::default()
+ };
+ unsafe {
+ ffi::ghostty_render_state_row_cells_get(
+ self.cells.raw,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE,
+ (&mut style as *mut ffi::GhosttyStyle).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(style.into())
+ }
+
+ pub fn fg_color(&self) -> Result<Option<RgbColor>, Error> {
+ let mut color = ffi::GhosttyColorRgb::default();
+ let result = unsafe {
+ ffi::ghostty_render_state_row_cells_get(
+ self.cells.raw,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR,
+ (&mut color as *mut ffi::GhosttyColorRgb).cast(),
+ )
+ };
+ match result {
+ ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(color.into())),
+ ffi::GhosttyResult_GHOSTTY_INVALID_VALUE => Ok(None),
+ other => Err(Error(other)),
+ }
+ }
+
+ pub fn bg_color(&self) -> Result<Option<RgbColor>, Error> {
+ let mut color = ffi::GhosttyColorRgb::default();
+ let result = unsafe {
+ ffi::ghostty_render_state_row_cells_get(
+ self.cells.raw,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR,
+ (&mut color as *mut ffi::GhosttyColorRgb).cast(),
+ )
+ };
+ match result {
+ ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(color.into())),
+ ffi::GhosttyResult_GHOSTTY_INVALID_VALUE => Ok(None),
+ other => Err(Error(other)),
+ }
+ }
+
+ pub fn grapheme_len(&self) -> Result<u32, Error> {
+ let mut len = 0u32;
+ // SAFETY: len output matches requested cell data type.
+ unsafe {
+ ffi::ghostty_render_state_row_cells_get(
+ self.cells.raw,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN,
+ (&mut len as *mut u32).cast(),
+ )
+ .into_result()?;
+ }
+ Ok(len)
+ }
+
+ pub fn graphemes(&self) -> Result<Vec<u32>, Error> {
+ let len = self.grapheme_len()? as usize;
+ let mut out = vec![0u32; len];
+ if len == 0 {
+ return Ok(out);
+ }
+ // SAFETY: out buffer is allocated for the grapheme count returned by the API.
+ unsafe {
+ ffi::ghostty_render_state_row_cells_get(
+ self.cells.raw,
+ ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF,
+ out.as_mut_ptr().cast::<c_void>(),
+ )
+ .into_result()?;
+ }
+ Ok(out)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn focus_encoding_matches_expected_sequences() {
+ assert_eq!(encode_focus(FocusEvent::Gained).unwrap(), b"\x1b[I");
+ assert_eq!(encode_focus(FocusEvent::Lost).unwrap(), b"\x1b[O");
+ }
+
+ #[test]
+ fn write_pty_callback_receives_terminal_query_responses() {
+ let mut terminal = Terminal::new(8, 3, 100).unwrap();
+ let responses = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
+ let sink = responses.clone();
+ terminal
+ .set_write_pty_callback(move |bytes| {
+ sink.lock().unwrap().extend_from_slice(bytes)
+ })
+ .unwrap();
+
+ terminal.write(b"\x1b[6n");
+
+ let output = responses.lock().unwrap().clone();
+ assert!(!output.is_empty());
+ assert!(String::from_utf8_lossy(&output).contains("R"));
+ }
+
+ #[test]
+ fn key_and_mouse_encoders_follow_terminal_state() {
+ let mut terminal = Terminal::new(80, 24, 0).unwrap();
+ terminal.mode_set(1, true).unwrap();
+ terminal.write(b"\x1b[>1u\x1b[?1000h\x1b[?1006h");
+
+ assert!(terminal.mode_get(1).unwrap());
+ assert_eq!(terminal.kitty_keyboard_flags().unwrap(), 1);
+ assert!(terminal.mouse_tracking_enabled().unwrap());
+
+ let mut key_encoder = KeyEncoder::new().unwrap();
+ key_encoder.set_from_terminal(&terminal);
+ let mut key_event = KeyEvent::new().unwrap();
+ key_event.set_action(ffi::GhosttyKeyAction_GHOSTTY_KEY_ACTION_PRESS);
+ key_event.set_key(KEY_A);
+ key_event.set_mods(MOD_CTRL | MOD_SHIFT);
+ key_event.set_utf8("A");
+ key_event.set_unshifted_codepoint('a' as u32);
+ let encoded_key = key_encoder.encode(&key_event).unwrap();
+ assert_eq!(encoded_key, b"\x1b[97;6u");
+
+ let mut mouse_encoder = MouseEncoder::new().unwrap();
+ mouse_encoder.set_from_terminal(&terminal);
+ mouse_encoder.set_size(80, 24, 1, 1);
+ let mut mouse_event = MouseEvent::new().unwrap();
+ mouse_event.set_action(ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_PRESS);
+ mouse_event.set_button(ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_LEFT);
+ mouse_event.set_position(0.0, 0.0);
+ let encoded_mouse = mouse_encoder.encode(&mouse_event).unwrap();
+ assert_eq!(encoded_mouse, b"\x1b[<0;1;1M");
+ }
+
+ #[test]
+ fn terminal_and_render_state_smoke_test() {
+ let mut terminal = Terminal::new(8, 3, 100).unwrap();
+ assert_eq!(terminal.cols().unwrap(), 8);
+ assert_eq!(terminal.rows().unwrap(), 3);
+
+ terminal.write(b"hello\r\nworld");
+
+ let mut render_state = RenderState::new().unwrap();
+ render_state.update(&terminal).unwrap();
+ assert_eq!(render_state.cols().unwrap(), 8);
+ assert_eq!(render_state.rows().unwrap(), 3);
+ assert_ne!(render_state.dirty().unwrap(), Dirty::Clean);
+
+ let mut row_iterator = RowIterator::new().unwrap();
+ let mut row_iter = render_state
+ .populate_row_iterator(&mut row_iterator)
+ .unwrap();
+ let mut row_cells = RowCells::new().unwrap();
+
+ let mut found_hello = false;
+ let mut found_world = false;
+ let mut row_index = 0usize;
+ while row_iter.next() {
+ let _ = row_iter.dirty().unwrap();
+ let mut cells = row_iter.populate_cells(&mut row_cells).unwrap();
+ let mut line = String::new();
+ while cells.next() {
+ let graphemes = cells.graphemes().unwrap();
+ if let Some(codepoint) = graphemes.first().copied() {
+ if let Some(ch) = char::from_u32(codepoint) {
+ line.push(ch);
+ }
+ } else {
+ line.push(' ');
+ }
+ }
+ let trimmed = line.trim_end().to_string();
+ if row_index == 0 {
+ found_hello = trimmed.starts_with("hello");
+ }
+ if row_index == 1 {
+ found_world = trimmed.starts_with("world");
+ }
+ row_index += 1;
+ }
+
+ assert!(found_hello);
+ assert!(found_world);
+
+ render_state.set_dirty(Dirty::Clean).unwrap();
+ assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
+ }
+}