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.rs4781
1 files changed, 0 insertions, 4781 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
deleted file mode 100644
index 43a70fb1..00000000
--- a/src/ghostty/mod.rs
+++ /dev/null
@@ -1,4781 +0,0 @@
-#![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::cell::Cell;
-use std::collections::hash_map::DefaultHasher;
-use std::collections::{HashMap, HashSet};
-use std::ffi::c_void;
-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;
-use std::sync::{Arc, Mutex, Once, OnceLock};
-
-use crate::pane_graphics_files::OwnedExport;
-mod native_source;
-
-pub use bindings as ffi;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct Error(ffi::GhosttyResult);
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum FormatterFormat {
- Plain,
- Vt,
-}
-
-impl FormatterFormat {
- fn as_raw(self) -> ffi::GhosttyFormatterFormat {
- match self {
- Self::Plain => ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_PLAIN,
- Self::Vt => ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_VT,
- }
- }
-}
-
-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,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum TerminalCompressionResult {
- Unsupported,
- Pending,
- Complete,
-}
-
-#[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 {
- 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,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ColorScheme {
- Light,
- Dark,
-}
-
-impl ColorScheme {
- fn as_raw(self) -> ffi::GhosttyColorScheme {
- match self {
- Self::Light => ffi::GhosttyColorScheme_GHOSTTY_COLOR_SCHEME_LIGHT,
- Self::Dark => ffi::GhosttyColorScheme_GHOSTTY_COLOR_SCHEME_DARK,
- }
- }
-}
-
-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: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ENTER;
-pub const KEY_UP: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_UP;
-pub const KEY_DOWN: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_DOWN;
-pub const KEY_LEFT: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_LEFT;
-pub const KEY_RIGHT: ffi::GhosttyKey = ffi::GhosttyKey_GHOSTTY_KEY_ARROW_RIGHT;
-pub const KEY_A: ffi::GhosttyKey = 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 MOUSE_FORMAT_SGR: ffi::GhosttyMouseFormat =
- ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_SGR;
-pub const MOUSE_FORMAT_SGR_PIXELS: ffi::GhosttyMouseFormat =
- ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_SGR_PIXELS;
-
-pub const MODE_APPLICATION_CURSOR_KEYS: u16 = 1;
-pub const MODE_FOCUS_EVENT: u16 = 1004;
-pub const MODE_MOUSE_UTF8: u16 = 1005;
-pub const MODE_MOUSE_SGR: u16 = 1006;
-pub const MODE_MOUSE_ALTERNATE_SCROLL: u16 = 1007;
-pub const MODE_MOUSE_SGR_PIXELS: u16 = 1016;
-pub const MODE_BRACKETED_PASTE: u16 = 2004;
-pub const MODE_SYNCHRONIZED_OUTPUT: u16 = 2026;
-pub const MODE_GRAPHEME_CLUSTER: u16 = 2027;
-pub const MODE_COLOR_SCHEME_REPORT: u16 = 2031;
-// These are documented in vendor/libghostty-vt/include/ghostty/vt/terminal.h,
-// but the generated bindings do not currently expose named constants for them.
-const TERMINAL_DATA_COLOR_FOREGROUND: ffi::GhosttyTerminalData = 18;
-const TERMINAL_DATA_COLOR_CURSOR: ffi::GhosttyTerminalData = 20;
-
-const KITTY_IMAGE_STORAGE_LIMIT_BYTES: u64 = 64 * 1024 * 1024;
-const APC_MAX_BYTES: usize = 16 * 1024 * 1024;
-const APC_MAX_BYTES_KITTY: usize = 16 * 1024 * 1024;
-pub(crate) const KITTY_UNICODE_PLACEHOLDER: u32 = 0x10EEEE;
-// The vendored C headers expose these placement fields, but the checked-in
-// generated bindings predate the names. Keep the explicit values aligned with
-// vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h.
-const KITTY_PLACEMENT_DATA_IS_VIRTUAL: ffi::GhosttyKittyGraphicsPlacementData = 3;
-const KITTY_PLACEMENT_DATA_COLUMNS: ffi::GhosttyKittyGraphicsPlacementData = 10;
-const KITTY_PLACEMENT_DATA_ROWS: ffi::GhosttyKittyGraphicsPlacementData = 11;
-
-static INSTALL_PNG_DECODER: Once = Once::new();
-static KITTY_PLACEHOLDER_DIACRITICS: OnceLock<HashMap<u32, u32>> = OnceLock::new();
-
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
-pub enum KittyImageFormat {
- Rgb,
- Rgba,
- Png,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct KittyImagePlacement {
- pub image_id: u32,
- pub placement_id: u32,
- pub z: i32,
- pub x_offset: u32,
- pub y_offset: u32,
- pub image_width: u32,
- pub image_height: u32,
- pub format: KittyImageFormat,
- pub data_len: usize,
- pub data_fingerprint: u64,
- pub data: Vec<u8>,
- pub(crate) source_file: Option<Arc<OwnedExport>>,
- pub render: KittyPlacementRenderInfo,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct KittyImageDescriptor {
- pub image_id: u32,
- pub placement_id: u32,
- pub image_width: u32,
- pub image_height: u32,
- pub format: KittyImageFormat,
- pub data_len: usize,
- pub data_fingerprint: u64,
- pub source_file: bool,
-}
-
-type KittyPlacementPayload = (KittyImageDescriptor, Vec<u8>, Option<Arc<OwnedExport>>);
-
-#[derive(Debug, Clone, Copy)]
-struct KittyImageFingerprintEntry {
- generation: u64,
- fingerprint: u64,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct KittyPlacementRenderInfo {
- pub pixel_width: u32,
- pub pixel_height: u32,
- pub grid_cols: u32,
- pub grid_rows: u32,
- pub viewport_col: i32,
- pub viewport_row: i32,
- pub source_x: u32,
- pub source_y: u32,
- pub source_width: u32,
- pub source_height: u32,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct KittyVirtualPlacementSpec {
- image_id: u32,
- placement_id: u32,
- columns: u32,
- rows: u32,
- z: i32,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct KittyVirtualCell {
- x: u16,
- y: u16,
- image_id_low: u32,
- image_id_high: Option<u32>,
- placement_id: Option<u32>,
- row: Option<u32>,
- col: Option<u32>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct KittyVirtualRun {
- x: u16,
- y: u16,
- image_id_low: u32,
- image_id_high: Option<u32>,
- placement_id: Option<u32>,
- row: u32,
- col: u32,
- width: u32,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct KittyVirtualPlacementGeometry {
- x_offset: u32,
- y_offset: u32,
- render: KittyPlacementRenderInfo,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CursorVisualStyle {
- Bar,
- Block,
- Underline,
- BlockHollow,
-}
-
-impl CursorVisualStyle {
- fn from_raw(value: ffi::GhosttyRenderStateCursorVisualStyle) -> Self {
- match value {
- ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK => {
- Self::Block
- }
- ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE => {
- Self::Underline
- }
- ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW => {
- Self::BlockHollow
- }
- _ => Self::Bar,
- }
- }
-}
-
-#[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)]
-pub struct RenderCursor {
- pub viewport: Option<CursorViewport>,
- pub visible: bool,
- pub blinking: bool,
- pub visual_style: CursorVisualStyle,
-}
-
-#[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,
- }
- }
-}
-
-pub fn default_palette() -> [RgbColor; 256] {
- let mut palette = [ffi::GhosttyColorRgb::default(); 256];
- unsafe { ffi::ghostty_color_palette_default(palette.as_mut_ptr()) };
- palette.map(Into::into)
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CellColor {
- Palette(u8),
- Rgb(RgbColor),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct CellStyle {
- pub fg_color: Option<CellColor>,
- pub bg_color: Option<CellColor>,
- pub underline_color: Option<CellColor>,
- 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 underline: u8,
- pub underlined: bool,
-}
-
-impl From<ffi::GhosttyStyle> for CellStyle {
- fn from(value: ffi::GhosttyStyle) -> Self {
- Self {
- fg_color: cell_color_from_style_color(value.fg_color),
- bg_color: cell_color_from_style_color(value.bg_color),
- underline_color: cell_color_from_style_color(value.underline_color),
- bold: value.bold,
- italic: value.italic,
- faint: value.faint,
- blink: value.blink,
- inverse: value.inverse,
- invisible: value.invisible,
- strikethrough: value.strikethrough,
- overline: value.overline,
- underline: normalize_underline_style(value.underline),
- underlined: value.underline != 0,
- }
- }
-}
-
-fn normalize_underline_style(value: std::os::raw::c_int) -> u8 {
- match value {
- 0..=5 => value as u8,
- _ => 1,
- }
-}
-
-fn cell_color_from_style_color(color: ffi::GhosttyStyleColor) -> Option<CellColor> {
- match color.tag {
- ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_PALETTE => {
- // SAFETY: Ghostty's tagged union stores `palette` when the tag is PALETTE.
- Some(CellColor::Palette(unsafe { color.value.palette }))
- }
- ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_RGB => {
- // SAFETY: Ghostty's tagged union stores `rgb` when the tag is RGB.
- Some(CellColor::Rgb(unsafe { color.value.rgb }.into()))
- }
- _ => None,
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct RenderColors {
- pub background: RgbColor,
- pub foreground: RgbColor,
- pub palette: [RgbColor; 256],
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CellWide {
- Narrow,
- Wide,
- SpacerTail,
- SpacerHead,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct ScreenTextCell {
- pub wide: CellWide,
- pub graphemes: Vec<u32>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct ScreenTextRow {
- pub cells: Vec<ScreenTextCell>,
- pub soft_wrapped: bool,
- pub wrap_continuation: bool,
-}
-
-impl CellWide {
- fn from_raw(value: ffi::GhosttyCellWide) -> Self {
- match value {
- ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW => Self::Narrow,
- ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_WIDE => Self::Wide,
- ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_TAIL => Self::SpacerTail,
- ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_SPACER_HEAD => Self::SpacerHead,
- _ => Self::Narrow,
- }
- }
-}
-
-type WritePtyCallback = dyn FnMut(&[u8]) + Send;
-
-const MAX_CLIPBOARD_BYTES: usize = 192 * 1024;
-
-#[derive(Default)]
-struct TerminalCallbackState {
- write_pty: Option<Box<WritePtyCallback>>,
- bell_count: u16,
- pwd_changes: Vec<Vec<u8>>,
- clipboard_writes: Vec<Vec<u8>>,
- size_report: ffi::GhosttySizeReportSize,
- color_scheme: Option<ColorScheme>,
-}
-
-unsafe extern "C" fn bell_trampoline(_terminal: ffi::GhosttyTerminal, userdata: *mut c_void) {
- if userdata.is_null() {
- return;
- }
- // SAFETY: userdata is the TerminalCallbackState installed with this terminal.
- let state = unsafe { &mut *userdata.cast::<TerminalCallbackState>() };
- state.bell_count = state.bell_count.saturating_add(1);
-}
-
-unsafe extern "C" fn color_scheme_trampoline(
- _terminal: ffi::GhosttyTerminal,
- userdata: *mut c_void,
- out_scheme: *mut ffi::GhosttyColorScheme,
-) -> bool {
- if userdata.is_null() || out_scheme.is_null() {
- return false;
- }
- let state = unsafe { &*userdata.cast::<TerminalCallbackState>() };
- let Some(color_scheme) = state.color_scheme else {
- return false;
- };
- unsafe {
- out_scheme.write(color_scheme.as_raw());
- }
- true
-}
-
-unsafe extern "C" fn size_trampoline(
- _terminal: ffi::GhosttyTerminal,
- userdata: *mut c_void,
- out_size: *mut ffi::GhosttySizeReportSize,
-) -> bool {
- if userdata.is_null() || out_size.is_null() {
- return false;
- }
- let state = unsafe { &*userdata.cast::<TerminalCallbackState>() };
- let size = state.size_report;
- if size.rows == 0 || size.columns == 0 || size.cell_width == 0 || size.cell_height == 0 {
- return false;
- }
- unsafe {
- out_size.write(size);
- }
- true
-}
-
-unsafe extern "C" fn write_pty_trampoline(
- _terminal: ffi::GhosttyTerminal,
- userdata: *mut c_void,
- data: *const u8,
- len: usize,
-) {
- if userdata.is_null() || (data.is_null() && len != 0) {
- return;
- }
- let state = unsafe { &mut *(userdata.cast::<TerminalCallbackState>()) };
- let Some(callback) = state.write_pty.as_mut() else {
- return;
- };
- let bytes = if len == 0 {
- &[]
- } else {
- unsafe { slice::from_raw_parts(data, len) }
- };
- callback(bytes);
-}
-
-unsafe extern "C" fn clipboard_write_trampoline(
- _terminal: ffi::GhosttyTerminal,
- userdata: *mut c_void,
- write: *const ffi::GhosttyClipboardWrite,
-) {
- if write.is_null() || unsafe { (*write).size } < mem::size_of::<ffi::GhosttyClipboardWrite>() {
- return;
- }
- // The reply and request are borrowed only until this callback returns.
- let Some(reply) = (unsafe { (*write).reply }) else {
- return;
- };
- let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
- // SAFETY: libghostty-vt owns these values for the synchronous callback.
- unsafe { capture_clipboard_write(userdata, write) }
- }))
- .unwrap_or(ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA);
- let response = ffi::GhosttyClipboardWriteReply {
- size: mem::size_of::<ffi::GhosttyClipboardWriteReply>(),
- result,
- remember: false,
- };
- unsafe { reply(write, &response) };
-}
-
-unsafe fn capture_clipboard_write(
- userdata: *mut c_void,
- write: *const ffi::GhosttyClipboardWrite,
-) -> ffi::GhosttyClipboardWriteResult {
- if userdata.is_null() || write.is_null() {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
- }
-
- let required_size = std::mem::offset_of!(ffi::GhosttyClipboardWrite, contents_len)
- + std::mem::size_of::<usize>();
- // SAFETY: size is the leading field of the live request.
- if unsafe { (*write).size } < required_size {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
- }
- // SAFETY: the size check covers every field accessed below.
- let request = unsafe { &*write };
- if request.location != ffi::GhosttyClipboardLocation_GHOSTTY_CLIPBOARD_LOCATION_STANDARD {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
- }
-
- // SAFETY: userdata is the TerminalCallbackState installed with this terminal.
- let state = unsafe { &mut *userdata.cast::<TerminalCallbackState>() };
- if request.contents_len == 0 {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS;
- }
- if request.contents_len != 1 {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
- }
- if request.contents.is_null() {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
- }
-
- // SAFETY: libghostty-vt keeps the single content and its strings alive for the callback.
- let content = unsafe { &*request.contents };
- // SAFETY: the MIME string is borrowed from the live callback request.
- let Some(mime) = (unsafe { borrowed_bytes(content.mime) }) else {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
- };
- let is_text = std::str::from_utf8(mime)
- .ok()
- .and_then(|mime| mime.split(';').next())
- .is_some_and(|mime| mime.trim().eq_ignore_ascii_case("text/plain"));
- if !is_text {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
- }
-
- // SAFETY: the data string is borrowed from the live callback request.
- let Some(bytes) = (unsafe { borrowed_bytes(content.data) }) else {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
- };
- if bytes.is_empty() {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
- }
- if bytes.len() > MAX_CLIPBOARD_BYTES {
- return ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
- }
- state.clipboard_writes.push(bytes.to_vec());
- ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS
-}
-
-unsafe fn borrowed_bytes<'a>(value: ffi::GhosttyString) -> Option<&'a [u8]> {
- if value.len == 0 {
- Some(&[])
- } else if value.ptr.is_null() {
- None
- } else {
- // SAFETY: the callback contract keeps pointer and length valid until return.
- Some(unsafe { slice::from_raw_parts(value.ptr, value.len) })
- }
-}
-
-unsafe extern "C" fn pwd_changed_trampoline(terminal: ffi::GhosttyTerminal, userdata: *mut c_void) {
- if terminal.is_null() || userdata.is_null() {
- return;
- }
- let mut pwd = ffi::GhosttyString::default();
- let result = unsafe {
- ffi::ghostty_terminal_get(
- terminal,
- ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_PWD,
- (&mut pwd as *mut ffi::GhosttyString).cast(),
- )
- };
- if result != ffi::GhosttyResult_GHOSTTY_SUCCESS || (pwd.ptr.is_null() && pwd.len != 0) {
- return;
- }
- let bytes = if pwd.len == 0 {
- Vec::new()
- } else {
- unsafe { slice::from_raw_parts(pwd.ptr, pwd.len) }.to_vec()
- };
- let state = unsafe { &mut *(userdata.cast::<TerminalCallbackState>()) };
- state.pwd_changes.push(bytes);
-}
-
-fn install_png_decoder_once() {
- INSTALL_PNG_DECODER.call_once(|| unsafe {
- let _ = ffi::ghostty_sys_set(
- ffi::GhosttySysOption_GHOSTTY_SYS_OPT_DECODE_PNG,
- (decode_png_trampoline as *const ()).cast(),
- );
- });
-}
-
-#[cfg(test)]
-thread_local! {
- static PNG_DECODE_CALLS: Cell<usize> = const { Cell::new(0) };
-}
-
-#[cfg(test)]
-mod native_source_tests;
-#[cfg(test)]
-mod png_forward_tests;
-
-unsafe extern "C" fn decode_png_trampoline(
- _userdata: *mut c_void,
- allocator: *const ffi::GhosttyAllocator,
- data: *const u8,
- data_len: usize,
- out: *mut ffi::GhosttySysImage,
-) -> bool {
- if data.is_null() || out.is_null() {
- return false;
- }
- let bytes = unsafe { slice::from_raw_parts(data, data_len) };
- #[cfg(test)]
- PNG_DECODE_CALLS.with(|calls| calls.set(calls.get() + 1));
- let Some(rgba) = decode_png_rgba(bytes) else {
- return false;
- };
- let ptr = unsafe { ffi::ghostty_alloc(allocator, rgba.data.len()) };
- if ptr.is_null() {
- return false;
- }
- unsafe {
- ptr::copy_nonoverlapping(rgba.data.as_ptr(), ptr, rgba.data.len());
- *out = ffi::GhosttySysImage {
- width: rgba.width,
- height: rgba.height,
- data: ptr,
- data_len: rgba.data.len(),
- };
- }
- true
-}
-
-struct DecodedPng {
- width: u32,
- height: u32,
- data: Vec<u8>,
-}
-
-fn decode_png_rgba(bytes: &[u8]) -> Option<DecodedPng> {
- let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
- decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
- let mut reader = decoder.read_info().ok()?;
- let mut buf = vec![0; reader.output_buffer_size()];
- let info = reader.next_frame(&mut buf).ok()?;
- let frame = &buf[..info.buffer_size()];
- if info.bit_depth != png::BitDepth::Eight {
- return None;
- }
-
- let data = match info.color_type {
- png::ColorType::Rgba => frame.to_vec(),
- png::ColorType::Rgb => {
- let mut out = Vec::with_capacity((info.width as usize) * (info.height as usize) * 4);
- for rgb in frame.chunks_exact(3) {
- out.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
- }
- out
- }
- png::ColorType::Grayscale => {
- let mut out = Vec::with_capacity((info.width as usize) * (info.height as usize) * 4);
- for gray in frame {
- out.extend_from_slice(&[*gray, *gray, *gray, 255]);
- }
- out
- }
- png::ColorType::GrayscaleAlpha => {
- let mut out = Vec::with_capacity((info.width as usize) * (info.height as usize) * 4);
- for ga in frame.chunks_exact(2) {
- out.extend_from_slice(&[ga[0], ga[0], ga[0], ga[1]]);
- }
- out
- }
- png::ColorType::Indexed => return None,
- };
-
- Some(DecodedPng {
- width: info.width,
- height: info.height,
- data,
- })
-}
-
-pub fn unicode_codepoint_width(codepoint: u32) -> u8 {
- unsafe { ffi::ghostty_unicode_codepoint_width(codepoint) }
-}
-
-pub fn unicode_grapheme_width(codepoints: &[u32]) -> (usize, u8) {
- let mut width = 0u8;
- let consumed = unsafe {
- ffi::ghostty_unicode_grapheme_width(codepoints.as_ptr(), codepoints.len(), &mut width)
- };
- (consumed, width)
-}
-
-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)
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub(crate) enum LinkTarget {
- Uri(String),
- Text { text: String, clicked_byte: usize },
-}
-
-pub struct Terminal {
- raw: ffi::GhosttyTerminal,
- max_scrollback: usize,
- #[cfg(windows)]
- tracked_row: ffi::GhosttyTrackedGridRef,
- callback_state: Box<TerminalCallbackState>,
- kitty_fingerprints: Mutex<HashMap<u32, KittyImageFingerprintEntry>>,
- kitty_empty_generation: Cell<Option<u64>>,
- kitty_png_forwarding: bool,
-}
-
-impl Terminal {
- pub fn new(cols: u16, rows: u16, max_scrollback: usize) -> Result<Self, Error> {
- let mut raw = ptr::null_mut();
- // SAFETY: valid out pointer, null allocator means default allocator.
- unsafe {
- ffi::ghostty_terminal_new(ptr::null(), &mut raw, cols, rows).into_result()?;
- }
-
- let mut terminal = Self {
- raw,
- max_scrollback,
- #[cfg(windows)]
- tracked_row: ptr::null_mut(),
- callback_state: Box::new(TerminalCallbackState {
- size_report: ffi::GhosttySizeReportSize {
- rows,
- columns: cols,
- ..Default::default()
- },
- ..Default::default()
- }),
- kitty_fingerprints: Mutex::new(HashMap::new()),
- kitty_empty_generation: Cell::new(None),
- kitty_png_forwarding: false,
- };
- let userdata = (&mut *terminal.callback_state as *mut TerminalCallbackState).cast();
- let glyph_protocol = false;
- let terminfo_name = ffi::GhosttyString {
- ptr: crate::pane::PANE_TERM.as_ptr().cast(),
- len: crate::pane::PANE_TERM.len(),
- };
- let grapheme_default = ffi::GhosttyTerminalModeConfig {
- mode: MODE_GRAPHEME_CLUSTER,
- value: true,
- };
- let clipboard_callback: ffi::GhosttyTerminalClipboardWriteFn =
- Some(clipboard_write_trampoline);
- unsafe {
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_TERMINFO_NAME,
- (&terminfo_name as *const ffi::GhosttyString).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES,
- (&max_scrollback as *const usize).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_MODE_DEFAULT,
- (&grapheme_default as *const ffi::GhosttyTerminalModeConfig).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_USERDATA,
- userdata,
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_SIZE,
- (size_trampoline as *const ()).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_BELL,
- (bell_trampoline as *const ()).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_PWD_CHANGED,
- (pwd_changed_trampoline as *const ()).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE,
- clipboard_callback.map_or(ptr::null(), |callback| (callback as *const ()).cast()),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_SCHEME,
- (color_scheme_trampoline as *const ()).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- terminal.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL,
- (&glyph_protocol as *const bool).cast(),
- )
- .into_result()?;
- }
- Ok(terminal)
- }
-
- 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(crate) fn compression_activity(&self) -> Result<u64, Error> {
- let mut activity = 0;
- // SAFETY: self.raw is a live terminal handle and activity is a valid out pointer.
- unsafe {
- ffi::ghostty_terminal_compression_activity(self.raw, &mut activity).into_result()?;
- }
- Ok(activity)
- }
-
- pub(crate) fn compress_incremental(&mut self) -> Result<TerminalCompressionResult, Error> {
- let mut result =
- ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED;
- // SAFETY: self.raw is a live terminal handle and result is a valid out pointer.
- unsafe {
- ffi::ghostty_terminal_compress(
- self.raw,
- ffi::GhosttyTerminalCompressionMode_GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL,
- &mut result,
- )
- .into_result()?;
- }
- match result {
- ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED => {
- Ok(TerminalCompressionResult::Unsupported)
- }
- ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_PENDING => {
- Ok(TerminalCompressionResult::Pending)
- }
- ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_COMPLETE => {
- Ok(TerminalCompressionResult::Complete)
- }
- _ => Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE)),
- }
- }
-
- pub fn set_default_palette(&mut self, palette: &[RgbColor; 256]) -> Result<(), Error> {
- let palette = palette.map(|color| ffi::GhosttyColorRgb {
- r: color.r,
- g: color.g,
- b: color.b,
- });
- unsafe {
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_COLOR_PALETTE,
- palette.as_ptr().cast(),
- )
- .into_result()
- }
- }
-
- pub fn default_palette(&self) -> Result<[RgbColor; 256], Error> {
- let mut out = [ffi::GhosttyColorRgb::default(); 256];
- // SAFETY: self.raw is a live terminal handle, and out is exactly the
- // 256-entry array this data kind writes.
- unsafe {
- ffi::ghostty_terminal_get(
- self.raw,
- ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT,
- out.as_mut_ptr().cast(),
- )
- .into_result()?;
- }
- Ok(out.map(Into::into))
- }
-
- pub fn resize(
- &mut self,
- cols: u16,
- rows: u16,
- cell_width_px: u32,
- cell_height_px: u32,
- ) -> Result<(), Error> {
- let size_report = ffi::GhosttySizeReportSize {
- rows,
- columns: cols,
- cell_width: cell_width_px,
- cell_height: cell_height_px,
- };
- // SAFETY: self.raw is valid and sizes are plain values.
- unsafe {
- ffi::ghostty_terminal_resize(
- self.raw,
- cols,
- rows,
- cell_width_px.max(1),
- cell_height_px.max(1),
- )
- .into_result()?;
- }
- self.callback_state.size_report = size_report;
- Ok(())
- }
-
- pub fn enable_kitty_graphics(&mut self) -> Result<(), Error> {
- install_png_decoder_once();
- let storage_limit = KITTY_IMAGE_STORAGE_LIMIT_BYTES;
- let enable_medium = true;
- let temp_dir = std::env::temp_dir();
- let temp_dir = temp_dir.to_string_lossy();
- let temp_dir = ffi::GhosttyString {
- ptr: temp_dir.as_ptr().cast(),
- len: temp_dir.len(),
- };
- unsafe {
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT,
- (&storage_limit as *const u64).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE,
- (&enable_medium as *const bool).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE,
- (&temp_dir as *const ffi::GhosttyString).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM,
- (&enable_medium as *const bool).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES,
- (&APC_MAX_BYTES as *const usize).cast(),
- )
- .into_result()?;
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY,
- (&APC_MAX_BYTES_KITTY as *const usize).cast(),
- )
- .into_result()?;
- }
- self.set_kitty_source_forwarding(true)?;
- Ok(())
- }
-
- pub fn set_kitty_source_forwarding(&mut self, enabled: bool) -> Result<(), Error> {
- native_source::set_forwarding(self.raw, enabled)
- }
-
- #[cfg(test)]
- fn set_kitty_png_forwarding(&mut self, enabled: bool) -> Result<(), Error> {
- unsafe {
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_PRESERVE_PNG,
- (&enabled as *const bool).cast(),
- )
- .into_result()?;
- }
- self.kitty_png_forwarding = enabled;
- Ok(())
- }
-
- pub fn set_write_pty_callback<F>(&mut self, callback: F) -> Result<(), Error>
- where
- F: FnMut(&[u8]) + Send + 'static,
- {
- unsafe {
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_WRITE_PTY,
- (write_pty_trampoline as *const ()).cast(),
- )
- .into_result()?;
- }
- self.callback_state.write_pty = Some(Box::new(callback));
- Ok(())
- }
-
- pub fn set_color_scheme(&mut self, color_scheme: Option<ColorScheme>) -> Option<ColorScheme> {
- mem::replace(&mut self.callback_state.color_scheme, color_scheme)
- }
-
- pub fn take_bell_count(&mut self) -> u16 {
- mem::take(&mut self.callback_state.bell_count)
- }
-
- pub fn take_pwd_changes(&mut self) -> Vec<Vec<u8>> {
- mem::take(&mut self.callback_state.pwd_changes)
- }
-
- pub fn take_clipboard_writes(&mut self) -> Vec<Vec<u8>> {
- mem::take(&mut self.callback_state.clipboard_writes)
- }
-
- pub fn mode_get(&self, mode: u16) -> Result<bool, Error> {
- let mut config = ffi::GhosttyTerminalModeConfig { mode, value: false };
- unsafe {
- ffi::ghostty_terminal_get(
- self.raw,
- ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MODE,
- (&mut config as *mut ffi::GhosttyTerminalModeConfig).cast(),
- )
- .into_result()?;
- }
- Ok(config.value)
- }
-
- pub fn mode_set(&mut self, mode: u16, value: bool) -> Result<(), Error> {
- let config = ffi::GhosttyTerminalModeConfig { mode, value };
- unsafe {
- ffi::ghostty_terminal_set(
- self.raw,
- ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_MODE,
- (&config as *const ffi::GhosttyTerminalModeConfig).cast(),
- )
- .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> {
- self.get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING)
- }
-
- pub fn modify_other_keys_enabled(&self) -> Result<bool, Error> {
- self.get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_MODIFY_OTHER_KEYS)
- }
-
- 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 max_scrollback(&self) -> usize {
- self.max_scrollback
- }
-
- 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,
- })
- }
-
- #[cfg(windows)]
- pub(crate) fn track_row(&mut self, y: u32) -> Option<usize> {
- let mut point = ffi::GhosttyPointCoordinate::default();
- let tag = ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN;
- let result =
- unsafe { ffi::ghostty_tracked_grid_ref_point(self.tracked_row, tag, &mut point) };
- let terminal = self.raw;
- let target = ghostty_viewport_point(0, y);
- unsafe {
- ffi::ghostty_tracked_grid_ref_free(self.tracked_row);
- self.tracked_row = ptr::null_mut();
- let _ = ffi::ghostty_terminal_grid_ref_track(terminal, target, &mut self.tracked_row);
- }
- (result == ffi::GhosttyResult_GHOSTTY_SUCCESS).then_some(point.y as usize)
- }
-
- pub fn screen_cell(&self, x: u16, y: u32) -> Result<(CellWide, Vec<u32>), Error> {
- let grid_ref = self.grid_ref(ghostty_screen_point(x, y))?;
- let wide = grid_ref_wide(&grid_ref)?;
- let graphemes = grid_ref_graphemes(&grid_ref)?;
- Ok((wide, graphemes))
- }
-
- pub(crate) fn screen_text_rows(&self) -> Result<Vec<ScreenTextRow>, Error> {
- self.screen_text_rows_range(0, usize::MAX)
- }
-
- pub(crate) fn screen_text_rows_range(
- &self,
- start_row: usize,
- end_row_exclusive: usize,
- ) -> Result<Vec<ScreenTextRow>, Error> {
- let total_rows = self.total_rows()?;
- let start_row = start_row.min(total_rows);
- let end_row_exclusive = end_row_exclusive.min(total_rows).max(start_row);
- let cols = self.cols()?;
- let mut rows = Vec::with_capacity(end_row_exclusive.saturating_sub(start_row));
- for y in start_row..end_row_exclusive {
- let Some(y) = u32::try_from(y).ok() else {
- break;
- };
- let mut grid_ref = self.grid_ref(ghostty_screen_point(0, y))?;
- let (soft_wrapped, wrap_continuation) = grid_ref_wrap_state(&grid_ref)?;
- let mut cells = Vec::with_capacity(usize::from(cols));
- for x in 0..cols {
- grid_ref.x = x;
- cells.push(ScreenTextCell {
- wide: grid_ref_wide(&grid_ref)?,
- graphemes: grid_ref_graphemes(&grid_ref)?,
- });
- }
- rows.push(ScreenTextRow {
- cells,
- soft_wrapped,
- wrap_continuation,
- });
- }
- Ok(rows)
- }
-
- fn viewport_graphemes_and_style(&self, x: u16, y: u32) -> Result<(Vec<u32>, CellStyle), Error> {
- let grid_ref = self.grid_ref(ghostty_viewport_point(x, y))?;
- let graphemes = grid_ref_graphemes(&grid_ref)?;
- let mut style = ffi::GhosttyStyle {
- size: mem::size_of::<ffi::GhosttyStyle>(),
- ..Default::default()
- };
- unsafe {
- ffi::ghostty_grid_ref_style(&grid_ref, &mut style).into_result()?;
- }
- Ok((graphemes, style.into()))
- }
-
- pub fn viewport_hyperlink_uri(&self, x: u16, y: u32) -> Result<Option<String>, Error> {
- let grid_ref = self.grid_ref(ghostty_viewport_point(x, y))?;
- grid_ref_hyperlink_uri(&grid_ref)
- }
-
- pub(crate) fn viewport_link_target(&self, x: u16, y: u32) -> Result<Option<LinkTarget>, Error> {
- Ok(self
- .viewport_link_selection(x, y)?
- .map(|(target, _)| target))
- }
-
- fn viewport_link_selection(
- &self,
- x: u16,
- y: u32,
- ) -> Result<Option<(LinkTarget, Option<ffi::GhosttySelection>)>, Error> {
- let mut clicked = self.grid_ref(ghostty_viewport_point(x, y))?;
- if grid_ref_wide(&clicked)? == CellWide::SpacerTail {
- clicked.x = clicked.x.saturating_sub(1);
- }
- if let Some(uri) = grid_ref_hyperlink_uri(&clicked)? {
- return Ok(Some((LinkTarget::Uri(uri), None)));
- }
- let graphemes = grid_ref_graphemes(&clicked)?;
- if graphemes
- .first()
- .copied()
- .and_then(char::from_u32)
- .is_none_or(char::is_whitespace)
- {
- return Ok(None);
- }
- // Unicode White_Space, matching Rust's char::is_whitespace URL boundaries.
- const BOUNDARIES: &[u32] = &[
- 9, 10, 11, 12, 13, 32, 0x85, 0xa0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004,
- 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000,
- ];
- let options = ffi::GhosttyTerminalSelectWordOptions {
- size: mem::size_of::<ffi::GhosttyTerminalSelectWordOptions>(),
- ref_: clicked,
- boundary_codepoints: BOUNDARIES.as_ptr(),
- boundary_codepoints_len: BOUNDARIES.len(),
- };
- let mut selection = ffi::GhosttySelection::default();
- // Bound work before formatting, even for an unbroken scrollback-sized token.
- let result = unsafe {
- ffi::ghostty_terminal_select_word_bounded(self.raw, &options, 8192, &mut selection)
- };
- if result == ffi::GhosttyResult_GHOSTTY_NO_VALUE {
- return Ok(None);
- }
- result.into_result()?;
- let text = self.format_selection(&selection, FormatterFormat::Plain, true, false)?;
- let prefix = self.format_selection(
- &ffi::GhosttySelection {
- end: clicked,
- ..selection
- },
- FormatterFormat::Plain,
- true,
- false,
- )?;
- let clicked_len: usize = graphemes
- .into_iter()
- .filter_map(char::from_u32)
- .map(char::len_utf8)
- .sum();
- Ok(Some((
- LinkTarget::Text {
- text,
- clicked_byte: prefix.len().saturating_sub(clicked_len),
- },
- Some(selection),
- )))
- }
-
- /// Resolve only the bounded plain-text token. OSC 8 regions are resolved by
- /// clients from frame hyperlink IDs; their full URI activation path is unchanged.
- pub(crate) fn viewport_link_regions(
- &self,
- x: u16,
- y: u32,
- resolve: fn(&str, usize) -> Option<std::ops::Range<usize>>,
- ) -> Result<Vec<crate::api::schema::PaneLinkRegion>, Error> {
- let cols = self.cols()?;
- let rows = self.rows()?;
- if x >= cols || y >= u32::from(rows) {
- return Ok(Vec::new());
- }
- let Some((LinkTarget::Text { text, clicked_byte }, Some(selection))) =
- self.viewport_link_selection(x, y)?
- else {
- return Ok(Vec::new());
- };
- let Some(range) = resolve(&text, clicked_byte) else {
- return Ok(Vec::new());
- };
- let point =
- |grid_ref: &ffi::GhosttyGridRef, clipped: (u16, u32)| -> Result<(u16, u32), Error> {
- let mut out = ffi::GhosttyPointCoordinate::default();
- let result = unsafe {
- ffi::ghostty_terminal_point_from_grid_ref(
- self.raw,
- grid_ref,
- ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_VIEWPORT,
- &mut out,
- )
- };
- if result == ffi::GhosttyResult_GHOSTTY_NO_VALUE {
- return Ok(clipped);
- }
- result.into_result()?;
- // C viewport coordinates can extend below the visible viewport
- // when scrolled up. Clip those as well as NO_VALUE endpoints.
- if out.y >= u32::from(rows) {
- return Ok(clipped);
- }
- Ok((out.x, out.y))
- };
- // The selected token contains the visible hovered cell, so an invisible
- // start/end necessarily lies above/below the viewport respectively.
- let (start_col, start_row) = point(&selection.start, (0, 0))?;
- let (end_col, end_row) = point(&selection.end, (cols - 1, u32::from(rows - 1)))?;
- let first = self.grid_ref(ghostty_viewport_point(start_col, start_row))?;
- let cell_len = |cell: &ffi::GhosttyGridRef| -> Result<usize, Error> {
- if matches!(
- grid_ref_wide(cell)?,
- CellWide::SpacerHead | CellWide::SpacerTail
- ) {
- return Ok(0);
- }
- Ok(grid_ref_graphemes(cell)?
- .into_iter()
- .filter_map(char::from_u32)
- .map(char::len_utf8)
- .sum())
- };
- // Format a prefix once, not once per cell. Then walk only the selected
- // visible cells, retaining one page/grid lookup per row.
- let prefix = self.format_selection(
- &ffi::GhosttySelection {
- end: first,
- ..selection
- },
- FormatterFormat::Plain,
- true,
- false,
- )?;
- let mut byte = prefix.len().saturating_sub(cell_len(&first)?);
- let mut regions: Vec<crate::api::schema::PaneLinkRegion> = Vec::new();
- for row in start_row..=end_row {
- let mut cell = self.grid_ref(ghostty_viewport_point(0, row))?;
- let left = if row == start_row { start_col } else { 0 };
- let right = if row == end_row { end_col } else { cols - 1 };
- for col in left..=right {
- cell.x = col;
- let wide = grid_ref_wide(&cell)?;
- if matches!(wide, CellWide::SpacerHead | CellWide::SpacerTail) {
- continue;
- }
- let len = cell_len(&cell)?;
- if byte < range.end && byte + len > range.start {
- let end = if wide == CellWide::Wide {
- (col + 1).min(cols - 1)
- } else {
- col
- };
- if let Some(last) = regions
- .last_mut()
- .filter(|last| u32::from(last.row) == row && last.end_col + 1 == col)
- {
- last.end_col = end;
- } else {
- regions.push(crate::api::schema::PaneLinkRegion {
- row: row as u16,
- start_col: col,
- end_col: end,
- });
- }
- }
- byte += len;
- }
- }
- Ok(regions)
- }
-
- fn grid_ref(&self, point: ffi::GhosttyPoint) -> Result<ffi::GhosttyGridRef, Error> {
- 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()?;
- }
- Ok(grid_ref)
- }
-
- pub fn read_text_viewport(
- &self,
- start: (u16, u32),
- end: (u16, u32),
- rectangle: bool,
- ) -> Result<String, Error> {
- self.read_formatted_selection(
- ghostty_viewport_point(start.0, start.1),
- ghostty_viewport_point(end.0, end.1),
- rectangle,
- FormatterFormat::Plain,
- true,
- true,
- )
- }
-
- pub fn read_ansi_viewport(
- &self,
- start: (u16, u32),
- end: (u16, u32),
- rectangle: bool,
- ) -> Result<String, Error> {
- self.read_formatted_selection(
- ghostty_viewport_point(start.0, start.1),
- ghostty_viewport_point(end.0, end.1),
- rectangle,
- FormatterFormat::Vt,
- false,
- true,
- )
- }
-
- pub fn read_text_screen(
- &self,
- start: (u16, u32),
- end: (u16, u32),
- rectangle: bool,
- ) -> Result<String, Error> {
- self.read_formatted_selection(
- ghostty_screen_point(start.0, start.1),
- ghostty_screen_point(end.0, end.1),
- rectangle,
- FormatterFormat::Plain,
- true,
- true,
- )
- }
-
- pub fn read_ansi_screen(
- &self,
- start: (u16, u32),
- end: (u16, u32),
- rectangle: bool,
- unwrap: bool,
- ) -> Result<String, Error> {
- self.read_formatted_selection(
- ghostty_screen_point(start.0, start.1),
- ghostty_screen_point(end.0, end.1),
- rectangle,
- FormatterFormat::Vt,
- unwrap,
- true,
- )
- }
-
- fn read_formatted_selection(
- &self,
- start: ffi::GhosttyPoint,
- end: ffi::GhosttyPoint,
- rectangle: bool,
- format: FormatterFormat,
- unwrap: bool,
- trim: bool,
- ) -> Result<String, Error> {
- let mut start_ref = ffi::GhosttyGridRef {
- size: mem::size_of::<ffi::GhosttyGridRef>(),
- ..Default::default()
- };
- let mut end_ref = ffi::GhosttyGridRef {
- size: mem::size_of::<ffi::GhosttyGridRef>(),
- ..Default::default()
- };
- unsafe {
- ffi::ghostty_terminal_grid_ref(self.raw, start, &mut start_ref).into_result()?;
- ffi::ghostty_terminal_grid_ref(self.raw, end, &mut end_ref).into_result()?;
- }
-
- let selection = ffi::GhosttySelection {
- size: mem::size_of::<ffi::GhosttySelection>(),
- start: start_ref,
- end: end_ref,
- rectangle,
- };
- self.format_selection(&selection, format, unwrap, trim)
- }
-
- fn format_selection(
- &self,
- selection: &ffi::GhosttySelection,
- format: FormatterFormat,
- unwrap: bool,
- trim: bool,
- ) -> Result<String, Error> {
- let mut formatter: ffi::GhosttyFormatter = ptr::null_mut();
- let options = ffi::GhosttyFormatterTerminalOptions {
- size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(),
- emit: format.as_raw(),
- unwrap,
- trim,
- extra: ffi::GhosttyFormatterTerminalExtra {
- size: mem::size_of::<ffi::GhosttyFormatterTerminalExtra>(),
- screen: ffi::GhosttyFormatterScreenExtra {
- size: mem::size_of::<ffi::GhosttyFormatterScreenExtra>(),
- ..Default::default()
- },
- ..Default::default()
- },
- selection,
- };
- unsafe {
- ffi::ghostty_formatter_terminal_new(ptr::null(), &mut formatter, self.raw, options)
- .into_result()?;
- }
-
- let mut out_ptr = ptr::null_mut();
- let mut out_len = 0usize;
- let result = unsafe {
- ffi::ghostty_formatter_format_alloc(formatter, ptr::null(), &mut out_ptr, &mut out_len)
- };
- unsafe {
- ffi::ghostty_formatter_free(formatter);
- }
- result.into_result()?;
-
- let text = if out_len == 0 {
- String::new()
- } else {
- let bytes = unsafe { slice::from_raw_parts(out_ptr.cast_const(), out_len) };
- String::from_utf8_lossy(bytes).into_owned()
- };
-
- if !out_ptr.is_null() {
- unsafe {
- ffi::ghostty_free(ptr::null(), out_ptr, out_len);
- }
- }
-
- Ok(text)
- }
-
- pub fn clear_screen(&mut self) -> bool {
- unsafe { ffi::ghostty_terminal_clear_screen(self.raw) }
- }
-
- 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 scroll_viewport_row(&mut self, row: usize) {
- let viewport = ffi::GhosttyTerminalScrollViewport {
- tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_ROW,
- value: ffi::GhosttyTerminalScrollViewportValue { row },
- };
- // 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)
- }
-
- pub fn cursor_y(&self) -> Result<u16, Error> {
- self.get_u16(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_CURSOR_Y)
- }
-
- pub fn effective_foreground_color(&self) -> Result<Option<RgbColor>, Error> {
- self.get_optional_rgb_color(TERMINAL_DATA_COLOR_FOREGROUND)
- }
-
- pub fn effective_cursor_color(&self) -> Result<Option<RgbColor>, Error> {
- self.get_optional_rgb_color(TERMINAL_DATA_COLOR_CURSOR)
- }
-
- pub(crate) fn width_px(&self) -> Result<u32, Error> {
- self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_WIDTH_PX)
- }
-
- pub(crate) fn height_px(&self) -> Result<u32, Error> {
- self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_HEIGHT_PX)
- }
-
- 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_u32(&self, data: ffi::GhosttyTerminalData) -> Result<u32, Error> {
- let mut out = 0u32;
- // SAFETY: out points to a u32 matching the requested terminal data type.
- unsafe {
- ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut u32).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 get_bool(&self, data: ffi::GhosttyTerminalData) -> Result<bool, Error> {
- let mut out = false;
- unsafe {
- ffi::ghostty_terminal_get(self.raw, data, (&mut out as *mut bool).cast())
- .into_result()?;
- }
- Ok(out)
- }
-
- fn get_optional_rgb_color(
- &self,
- data: ffi::GhosttyTerminalData,
- ) -> Result<Option<RgbColor>, Error> {
- let mut out = ffi::GhosttyColorRgb::default();
- let result = unsafe {
- ffi::ghostty_terminal_get(
- self.raw,
- data,
- (&mut out as *mut ffi::GhosttyColorRgb).cast(),
- )
- };
- match result {
- ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(out.into())),
- ffi::GhosttyResult_GHOSTTY_NO_VALUE => Ok(None),
- other => Err(Error(other)),
- }
- }
-
- fn kitty_graphics(&self) -> Result<ffi::GhosttyKittyGraphics, Error> {
- let mut graphics: ffi::GhosttyKittyGraphics = ptr::null_mut();
- unsafe {
- ffi::ghostty_terminal_get(
- self.raw,
- ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS,
- (&mut graphics as *mut ffi::GhosttyKittyGraphics).cast(),
- )
- .into_result()?;
- }
- Ok(graphics)
- }
-
- pub fn kitty_graphics_generation(&self) -> Result<u64, Error> {
- let graphics = self.kitty_graphics()?;
- if graphics.is_null() {
- return Ok(0);
- }
- kitty_graphics_u64(
- graphics,
- ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION,
- )
- }
-
- pub(crate) fn kitty_graphics_may_have_placements(&self) -> Result<bool, Error> {
- let generation = self.kitty_graphics_generation()?;
- Ok(generation != 0 && self.kitty_empty_generation.get() != Some(generation))
- }
-
- pub fn kitty_image_placements(&self) -> Result<Vec<KittyImagePlacement>, Error> {
- self.kitty_image_placements_with_data_filter(|_| true)
- }
-
- pub fn kitty_image_placements_with_data_filter<F>(
- &self,
- mut needs_data: F,
- ) -> Result<Vec<KittyImagePlacement>, Error>
- where
- F: FnMut(KittyImageDescriptor) -> bool,
- {
- let graphics = self.kitty_graphics()?;
- if graphics.is_null() {
- return Ok(Vec::new());
- }
- let generation = kitty_graphics_u64(
- graphics,
- ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION,
- )?;
- if generation == 0 || self.kitty_empty_generation.get() == Some(generation) {
- return Ok(Vec::new());
- }
-
- let mut iterator: ffi::GhosttyKittyGraphicsPlacementIterator = ptr::null_mut();
- unsafe {
- ffi::ghostty_kitty_graphics_placement_iterator_new(ptr::null(), &mut iterator)
- .into_result()?;
- ffi::ghostty_kitty_graphics_get(
- graphics,
- ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR,
- (&mut iterator as *mut ffi::GhosttyKittyGraphicsPlacementIterator).cast(),
- )
- .into_result()?;
- }
- let _guard = KittyPlacementIteratorGuard { raw: iterator };
-
- let mut placements = Vec::new();
- let mut storage_has_placements = false;
- while unsafe { ffi::ghostty_kitty_graphics_placement_next(iterator) } {
- storage_has_placements = true;
- if let Some(placement) =
- self.kitty_image_placement(graphics, iterator, &mut needs_data)?
- {
- placements.push(placement);
- }
- }
- if !storage_has_placements {
- self.kitty_empty_generation.set(Some(generation));
- self.prune_kitty_fingerprints(&[]);
- return Ok(Vec::new());
- }
-
- placements.extend(self.kitty_virtual_image_placements(graphics, &mut needs_data)?);
- placements.sort_by_key(|placement| placement.z);
- self.prune_kitty_fingerprints(&placements);
- Ok(placements)
- }
-
- /// Fingerprint for `image`, cached per image id and recomputed only when
- /// the image's generation changes.
- fn kitty_image_fingerprint_cached(
- &self,
- image: ffi::GhosttyKittyGraphicsImage,
- image_id: u32,
- data: (*const u8, usize),
- image_width: u32,
- image_height: u32,
- format: KittyImageFormat,
- ) -> u64 {
- let (data_ptr, data_len) = data;
- let Ok(generation) = kitty_image_u64(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_GENERATION,
- ) else {
- return kitty_image_fingerprint(data_ptr, data_len, image_width, image_height, format);
- };
-
- if let Ok(cache) = self.kitty_fingerprints.lock() {
- if let Some(entry) = cache.get(&image_id) {
- if entry.generation == generation {
- return entry.fingerprint;
- }
- }
- }
-
- let fingerprint =
- kitty_image_fingerprint(data_ptr, data_len, image_width, image_height, format);
- if let Ok(mut cache) = self.kitty_fingerprints.lock() {
- cache.insert(
- image_id,
- KittyImageFingerprintEntry {
- generation,
- fingerprint,
- },
- );
- }
- fingerprint
- }
-
- fn prune_kitty_fingerprints(&self, placements: &[KittyImagePlacement]) {
- if let Ok(mut cache) = self.kitty_fingerprints.lock() {
- if cache.is_empty() {
- return;
- }
- let live: HashSet<u32> = placements
- .iter()
- .map(|placement| placement.image_id)
- .collect();
- cache.retain(|image_id, _| live.contains(image_id));
- }
- }
-
- // Retained files are authoritative: never ask the native lazy decoder for
- // pixels just to describe a placement or calculate its cache identity.
- fn kitty_placement_payload<F>(
- &self,
- image: ffi::GhosttyKittyGraphicsImage,
- image_id: u32,
- placement_id: u32,
- image_width: u32,
- image_height: u32,
- decoded_format: KittyImageFormat,
- needs_data: &mut F,
- ) -> Result<KittyPlacementPayload, Error>
- where
- F: FnMut(KittyImageDescriptor) -> bool,
- {
- let source_file = native_source::image_source(image)?;
- let (format, data_ptr, data_len, data_fingerprint) = if let Some(source) = &source_file {
- (
- KittyImageFormat::Rgba,
- ptr::null(),
- source.len(),
- source.fingerprint(),
- )
- } else {
- let (format, data_ptr, data_len) =
- kitty_image_transmission_payload(image, decoded_format, self.kitty_png_forwarding)?;
- let fingerprint = self.kitty_image_fingerprint_cached(
- image,
- image_id,
- (data_ptr, data_len),
- image_width,
- image_height,
- format,
- );
- (format, data_ptr, data_len, fingerprint)
- };
- let descriptor = KittyImageDescriptor {
- image_id,
- placement_id,
- image_width,
- image_height,
- format,
- data_len,
- data_fingerprint,
- source_file: source_file.is_some(),
- };
- let data = if needs_data(descriptor) {
- match &source_file {
- Some(source) => source
- .copy_rgba()
- .map_err(|_| Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE))?,
- None => kitty_image_data_from_ptr(data_ptr, data_len),
- }
- } else {
- Vec::new()
- };
- Ok((descriptor, data, source_file))
- }
-
- fn kitty_image_placement<F>(
- &self,
- graphics: ffi::GhosttyKittyGraphics,
- iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
- needs_data: &mut F,
- ) -> Result<Option<KittyImagePlacement>, Error>
- where
- F: FnMut(KittyImageDescriptor) -> bool,
- {
- let image_id = kitty_placement_u32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID,
- )?;
- if kitty_placement_bool(iterator, KITTY_PLACEMENT_DATA_IS_VIRTUAL)? {
- return Ok(None);
- }
- let image = unsafe { ffi::ghostty_kitty_graphics_image(graphics, image_id) };
- if image.is_null() {
- return Ok(None);
- }
-
- let mut raw_info = ffi::GhosttyKittyGraphicsPlacementRenderInfo {
- size: mem::size_of::<ffi::GhosttyKittyGraphicsPlacementRenderInfo>(),
- ..Default::default()
- };
- unsafe {
- ffi::ghostty_kitty_graphics_placement_render_info(
- iterator,
- image,
- self.raw,
- &mut raw_info,
- )
- .into_result()?;
- }
- if !raw_info.viewport_visible {
- return Ok(None);
- }
-
- let image_width = kitty_image_u32(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_WIDTH,
- )?;
- let image_height = kitty_image_u32(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_HEIGHT,
- )?;
- let format = kitty_image_format(image)?;
- let compression = kitty_image_compression(image)?;
- if compression != ffi::GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE {
- return Ok(None);
- }
- let placement_id = kitty_placement_u32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID,
- )?;
- let (descriptor, data, source_file) = self.kitty_placement_payload(
- image,
- image_id,
- placement_id,
- image_width,
- image_height,
- format,
- needs_data,
- )?;
- let KittyImageDescriptor {
- format,
- data_len,
- data_fingerprint,
- ..
- } = descriptor;
- let x_offset = kitty_placement_u32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET,
- )?;
- let y_offset = kitty_placement_u32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET,
- )?;
- let z = kitty_placement_i32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z,
- )?;
-
- Ok(Some(KittyImagePlacement {
- image_id,
- placement_id,
- z,
- x_offset,
- y_offset,
- image_width,
- image_height,
- format,
- data_len,
- data_fingerprint,
- data,
- source_file,
- render: KittyPlacementRenderInfo {
- pixel_width: raw_info.pixel_width,
- pixel_height: raw_info.pixel_height,
- grid_cols: raw_info.grid_cols,
- grid_rows: raw_info.grid_rows,
- viewport_col: raw_info.viewport_col,
- viewport_row: raw_info.viewport_row,
- source_x: raw_info.source_x,
- source_y: raw_info.source_y,
- source_width: raw_info.source_width,
- source_height: raw_info.source_height,
- },
- }))
- }
-
- fn kitty_virtual_image_placements<F>(
- &self,
- graphics: ffi::GhosttyKittyGraphics,
- needs_data: &mut F,
- ) -> Result<Vec<KittyImagePlacement>, Error>
- where
- F: FnMut(KittyImageDescriptor) -> bool,
- {
- let specs = kitty_virtual_placement_specs(graphics)?;
- if specs.is_empty() {
- return Ok(Vec::new());
- }
-
- let viewport_cols = self.cols()?.max(1);
- let viewport_rows = self.rows()?.max(1);
- let cell_width = (self.width_px()? / u32::from(viewport_cols)).max(1);
- let cell_height = (self.height_px()? / u32::from(viewport_rows)).max(1);
- let mut runs = Vec::new();
- for y in 0..viewport_rows {
- let mut current: Option<KittyVirtualRun> = None;
- for x in 0..viewport_cols {
- let (graphemes, style) = self.viewport_graphemes_and_style(x, u32::from(y))?;
- let cell = kitty_virtual_cell(x, y, &graphemes, style);
- match cell {
- Some(cell) => {
- if let Some(run) = current.as_mut() {
- if run.append(cell) {
- continue;
- }
- runs.push(*run);
- }
- current = Some(KittyVirtualRun::from_cell(cell));
- }
- None => {
- if let Some(run) = current.take() {
- runs.push(run);
- }
- }
- }
- }
- if let Some(run) = current {
- runs.push(run);
- }
- }
-
- let mut placements = Vec::new();
- for run in runs {
- let image_id = run.image_id();
- let Some(spec) = find_virtual_placement_spec(&specs, image_id, run.placement_id())
- else {
- continue;
- };
- let image = unsafe { ffi::ghostty_kitty_graphics_image(graphics, image_id) };
- if image.is_null() {
- continue;
- }
- let image_width = kitty_image_u32(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_WIDTH,
- )?;
- let image_height = kitty_image_u32(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_HEIGHT,
- )?;
- let format = kitty_image_format(image)?;
- let compression = kitty_image_compression(image)?;
- if compression != ffi::GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE
- {
- continue;
- }
- let Some(geometry) = kitty_virtual_placement_geometry(
- run,
- *spec,
- image_width,
- image_height,
- cell_width,
- cell_height,
- ) else {
- continue;
- };
- let placement_id = run.synthetic_placement_id();
- let (descriptor, data, source_file) = self.kitty_placement_payload(
- image,
- image_id,
- placement_id,
- image_width,
- image_height,
- format,
- needs_data,
- )?;
- let KittyImageDescriptor {
- format,
- data_len,
- data_fingerprint,
- ..
- } = descriptor;
- placements.push(KittyImagePlacement {
- image_id,
- placement_id,
- z: spec.z,
- x_offset: geometry.x_offset,
- y_offset: geometry.y_offset,
- image_width,
- image_height,
- format,
- data_len,
- data_fingerprint,
- data,
- source_file,
- render: geometry.render,
- });
- }
-
- Ok(placements)
- }
-
- fn raw(&self) -> ffi::GhosttyTerminal {
- self.raw
- }
-}
-
-struct KittyPlacementIteratorGuard {
- raw: ffi::GhosttyKittyGraphicsPlacementIterator,
-}
-
-impl Drop for KittyPlacementIteratorGuard {
- fn drop(&mut self) {
- unsafe { ffi::ghostty_kitty_graphics_placement_iterator_free(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 {
- #[cfg(windows)]
- ffi::ghostty_tracked_grid_ref_free(self.tracked_row);
- ffi::ghostty_terminal_free(self.raw);
- }
- }
-}
-
-fn ghostty_viewport_point(x: u16, y: u32) -> ffi::GhosttyPoint {
- ffi::GhosttyPoint {
- tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_VIEWPORT,
- value: ffi::GhosttyPointValue {
- coordinate: ffi::GhosttyPointCoordinate { x, y },
- },
- }
-}
-
-fn ghostty_screen_point(x: u16, y: u32) -> ffi::GhosttyPoint {
- ffi::GhosttyPoint {
- tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN,
- value: ffi::GhosttyPointValue {
- coordinate: ffi::GhosttyPointCoordinate { x, y },
- },
- }
-}
-
-fn grid_ref_graphemes(grid_ref: &ffi::GhosttyGridRef) -> Result<Vec<u32>, Error> {
- 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)
-}
-
-fn grid_ref_wide(grid_ref: &ffi::GhosttyGridRef) -> Result<CellWide, Error> {
- let mut raw = ffi::GhosttyCell::default();
- unsafe {
- ffi::ghostty_grid_ref_cell(grid_ref, &mut raw).into_result()?;
- }
-
- let mut wide = ffi::GhosttyCellWide_GHOSTTY_CELL_WIDE_NARROW;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE,
- (&mut wide as *mut ffi::GhosttyCellWide).cast(),
- )
- .into_result()?;
- }
- Ok(CellWide::from_raw(wide))
-}
-
-fn grid_ref_wrap_state(grid_ref: &ffi::GhosttyGridRef) -> Result<(bool, bool), Error> {
- let mut row = 0;
- unsafe {
- ffi::ghostty_grid_ref_row(grid_ref, &mut row).into_result()?;
- }
- let mut soft_wrapped = false;
- let mut wrap_continuation = false;
- unsafe {
- ffi::ghostty_row_get(
- row,
- ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP,
- (&mut soft_wrapped as *mut bool).cast(),
- )
- .into_result()?;
- ffi::ghostty_row_get(
- row,
- ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP_CONTINUATION,
- (&mut wrap_continuation as *mut bool).cast(),
- )
- .into_result()?;
- }
- Ok((soft_wrapped, wrap_continuation))
-}
-
-fn kitty_placement_u32(
- iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
- data: ffi::GhosttyKittyGraphicsPlacementData,
-) -> Result<u32, Error> {
- let mut out = 0u32;
- unsafe {
- ffi::ghostty_kitty_graphics_placement_get(iterator, data, (&mut out as *mut u32).cast())
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_placement_i32(
- iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
- data: ffi::GhosttyKittyGraphicsPlacementData,
-) -> Result<i32, Error> {
- let mut out = 0i32;
- unsafe {
- ffi::ghostty_kitty_graphics_placement_get(iterator, data, (&mut out as *mut i32).cast())
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_placement_bool(
- iterator: ffi::GhosttyKittyGraphicsPlacementIterator,
- data: ffi::GhosttyKittyGraphicsPlacementData,
-) -> Result<bool, Error> {
- let mut out = false;
- unsafe {
- ffi::ghostty_kitty_graphics_placement_get(iterator, data, (&mut out as *mut bool).cast())
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_virtual_placement_specs(
- graphics: ffi::GhosttyKittyGraphics,
-) -> Result<Vec<KittyVirtualPlacementSpec>, Error> {
- let mut iterator: ffi::GhosttyKittyGraphicsPlacementIterator = ptr::null_mut();
- unsafe {
- ffi::ghostty_kitty_graphics_placement_iterator_new(ptr::null(), &mut iterator)
- .into_result()?;
- ffi::ghostty_kitty_graphics_get(
- graphics,
- ffi::GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR,
- (&mut iterator as *mut ffi::GhosttyKittyGraphicsPlacementIterator).cast(),
- )
- .into_result()?;
- }
- let _guard = KittyPlacementIteratorGuard { raw: iterator };
-
- let mut specs = Vec::new();
- while unsafe { ffi::ghostty_kitty_graphics_placement_next(iterator) } {
- if !kitty_placement_bool(iterator, KITTY_PLACEMENT_DATA_IS_VIRTUAL)? {
- continue;
- }
- specs.push(KittyVirtualPlacementSpec {
- image_id: kitty_placement_u32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID,
- )?,
- placement_id: kitty_placement_u32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID,
- )?,
- columns: kitty_placement_u32(iterator, KITTY_PLACEMENT_DATA_COLUMNS)?,
- rows: kitty_placement_u32(iterator, KITTY_PLACEMENT_DATA_ROWS)?,
- z: kitty_placement_i32(
- iterator,
- ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z,
- )?,
- });
- }
- Ok(specs)
-}
-
-fn find_virtual_placement_spec(
- specs: &[KittyVirtualPlacementSpec],
- image_id: u32,
- placement_id: u32,
-) -> Option<&KittyVirtualPlacementSpec> {
- if placement_id > 0 {
- specs
- .iter()
- .find(|spec| spec.image_id == image_id && spec.placement_id == placement_id)
- } else {
- specs.iter().find(|spec| spec.image_id == image_id)
- }
-}
-
-fn kitty_virtual_cell(
- x: u16,
- y: u16,
- graphemes: &[u32],
- style: CellStyle,
-) -> Option<KittyVirtualCell> {
- if graphemes.first().copied() != Some(KITTY_UNICODE_PLACEHOLDER) {
- return None;
- }
- let image_id_low = style
- .fg_color
- .map(kitty_placeholder_color_to_id)
- .unwrap_or(0);
- let placement_id = style
- .underline_color
- .map(kitty_placeholder_color_to_id)
- .filter(|id| *id != 0);
- let row = graphemes
- .get(1)
- .and_then(|codepoint| kitty_placeholder_diacritic_index(*codepoint));
- let col = graphemes
- .get(2)
- .and_then(|codepoint| kitty_placeholder_diacritic_index(*codepoint));
- let image_id_high = graphemes
- .get(3)
- .and_then(|codepoint| kitty_placeholder_diacritic_index(*codepoint))
- .filter(|high| *high <= u32::from(u8::MAX));
-
- Some(KittyVirtualCell {
- x,
- y,
- image_id_low,
- image_id_high,
- placement_id,
- row,
- col,
- })
-}
-
-fn kitty_placeholder_color_to_id(color: CellColor) -> u32 {
- match color {
- CellColor::Palette(value) => value.into(),
- CellColor::Rgb(color) => {
- (u32::from(color.r) << 16) | (u32::from(color.g) << 8) | u32::from(color.b)
- }
- }
-}
-
-fn kitty_placeholder_diacritic_index(codepoint: u32) -> Option<u32> {
- let map = KITTY_PLACEHOLDER_DIACRITICS.get_or_init(|| {
- // Reuse Ghostty's vendored table so Herdr decodes the same placeholder
- // row/column diacritics that libghostty accepts.
- let source =
- include_str!("../../vendor/libghostty-vt/src/terminal/kitty/graphics_unicode.zig");
- let mut map = HashMap::new();
- let mut in_table = false;
- for line in source.lines() {
- let line = line.trim();
- if line.starts_with("const diacritics:") {
- in_table = true;
- continue;
- }
- if !in_table {
- continue;
- }
- if line == "};" {
- break;
- }
- let Some(hex) = line
- .strip_prefix("0x")
- .and_then(|value| value.strip_suffix(','))
- else {
- continue;
- };
- if let Ok(value) = u32::from_str_radix(hex, 16) {
- map.insert(value, map.len() as u32);
- }
- }
- map
- });
- map.get(&codepoint).copied()
-}
-
-fn kitty_virtual_placement_geometry(
- run: KittyVirtualRun,
- spec: KittyVirtualPlacementSpec,
- image_width: u32,
- image_height: u32,
- cell_width: u32,
- cell_height: u32,
-) -> Option<KittyVirtualPlacementGeometry> {
- let grid_cols = if spec.columns == 0 {
- image_width.saturating_add(cell_width - 1) / cell_width
- } else {
- spec.columns
- }
- .max(1);
- let grid_rows = if spec.rows == 0 {
- image_height.saturating_add(cell_height - 1) / cell_height
- } else {
- spec.rows
- }
- .max(1);
-
- if run.col >= grid_cols || run.row >= grid_rows {
- return None;
- }
- let visible_cols = run.width.min(grid_cols.saturating_sub(run.col)).max(1);
- let visible_rows = 1;
- let source_x = scale_u32(run.col, image_width, grid_cols);
- let source_y = scale_u32(run.row, image_height, grid_rows);
- let source_width = scale_u32(visible_cols, image_width, grid_cols)
- .max(1)
- .min(image_width.saturating_sub(source_x));
- let source_height = scale_u32(visible_rows, image_height, grid_rows)
- .max(1)
- .min(image_height.saturating_sub(source_y));
- if source_width == 0 || source_height == 0 {
- return None;
- }
-
- Some(KittyVirtualPlacementGeometry {
- x_offset: 0,
- y_offset: 0,
- render: KittyPlacementRenderInfo {
- pixel_width: visible_cols.saturating_mul(cell_width).max(1),
- pixel_height: visible_rows.saturating_mul(cell_height).max(1),
- grid_cols: visible_cols,
- grid_rows: visible_rows,
- viewport_col: i32::from(run.x),
- viewport_row: i32::from(run.y),
- source_x,
- source_y,
- source_width,
- source_height,
- },
- })
-}
-
-fn scale_u32(value: u32, source: u32, dest: u32) -> u32 {
- ((u64::from(value)).saturating_mul(u64::from(source)) / u64::from(dest.max(1)))
- .min(u64::from(u32::MAX)) as u32
-}
-
-impl KittyVirtualRun {
- fn from_cell(cell: KittyVirtualCell) -> Self {
- Self {
- x: cell.x,
- y: cell.y,
- image_id_low: cell.image_id_low,
- image_id_high: cell.image_id_high,
- placement_id: cell.placement_id,
- row: cell.row.unwrap_or(0),
- col: cell.col.unwrap_or(0),
- width: 1,
- }
- }
-
- fn append(&mut self, cell: KittyVirtualCell) -> bool {
- if self.image_id_low != cell.image_id_low
- || self.placement_id != cell.placement_id
- || cell.row.is_some_and(|row| row != self.row)
- || cell.col.is_some_and(|col| col != self.col + self.width)
- || cell
- .image_id_high
- .is_some_and(|high| Some(high) != self.image_id_high)
- {
- return false;
- }
- self.width += 1;
- true
- }
-
- fn image_id(self) -> u32 {
- self.image_id_low | (self.image_id_high.unwrap_or(0) << 24)
- }
-
- fn placement_id(self) -> u32 {
- self.placement_id.unwrap_or(0)
- }
-
- fn synthetic_placement_id(self) -> u32 {
- let mut hasher = DefaultHasher::new();
- self.image_id().hash(&mut hasher);
- self.placement_id().hash(&mut hasher);
- self.row.hash(&mut hasher);
- self.col.hash(&mut hasher);
- self.width.hash(&mut hasher);
- self.x.hash(&mut hasher);
- self.y.hash(&mut hasher);
- 1 + ((hasher.finish() as u32) % 900_000)
- }
-}
-
-fn kitty_graphics_u64(
- graphics: ffi::GhosttyKittyGraphics,
- data: ffi::GhosttyKittyGraphicsData,
-) -> Result<u64, Error> {
- let mut out = 0u64;
- unsafe {
- ffi::ghostty_kitty_graphics_get(graphics, data, (&mut out as *mut u64).cast())
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_image_u32(
- image: ffi::GhosttyKittyGraphicsImage,
- data: ffi::GhosttyKittyGraphicsImageData,
-) -> Result<u32, Error> {
- let mut out = 0u32;
- unsafe {
- ffi::ghostty_kitty_graphics_image_get(image, data, (&mut out as *mut u32).cast())
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_image_u64(
- image: ffi::GhosttyKittyGraphicsImage,
- data: ffi::GhosttyKittyGraphicsImageData,
-) -> Result<u64, Error> {
- let mut out = 0u64;
- unsafe {
- ffi::ghostty_kitty_graphics_image_get(image, data, (&mut out as *mut u64).cast())
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_image_format(image: ffi::GhosttyKittyGraphicsImage) -> Result<KittyImageFormat, Error> {
- let mut out = ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGBA;
- unsafe {
- ffi::ghostty_kitty_graphics_image_get(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_FORMAT,
- (&mut out as *mut ffi::GhosttyKittyImageFormat).cast(),
- )
- .into_result()?;
- }
- match out {
- ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGB => Ok(KittyImageFormat::Rgb),
- ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGBA => Ok(KittyImageFormat::Rgba),
- ffi::GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_PNG => Ok(KittyImageFormat::Png),
- _ => Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE)),
- }
-}
-
-fn kitty_image_compression(
- image: ffi::GhosttyKittyGraphicsImage,
-) -> Result<ffi::GhosttyKittyImageCompression, Error> {
- let mut out = ffi::GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE;
- unsafe {
- ffi::ghostty_kitty_graphics_image_get(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION,
- (&mut out as *mut ffi::GhosttyKittyImageCompression).cast(),
- )
- .into_result()?;
- }
- Ok(out)
-}
-
-fn kitty_image_transmission_payload(
- image: ffi::GhosttyKittyGraphicsImage,
- decoded_format: KittyImageFormat,
- forwarding: bool,
-) -> Result<(KittyImageFormat, *const u8, usize), Error> {
- if !forwarding {
- let (ptr, len) = kitty_image_data_ptr_len(image)?;
- return Ok((decoded_format, ptr, len));
- }
- let mut ptr_out: *const u8 = ptr::null();
- let result = unsafe {
- ffi::ghostty_kitty_graphics_image_get(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_ENCODED_PNG_PTR,
- (&mut ptr_out as *mut *const u8).cast(),
- )
- };
- if result == ffi::GhosttyResult_GHOSTTY_NO_VALUE {
- let (ptr, len) = kitty_image_data_ptr_len(image)?;
- return Ok((decoded_format, ptr, len));
- }
- result.into_result()?;
- let mut len = 0usize;
- unsafe {
- ffi::ghostty_kitty_graphics_image_get(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_ENCODED_PNG_LEN,
- (&mut len as *mut usize).cast(),
- )
- .into_result()?;
- }
- Ok((KittyImageFormat::Png, ptr_out, len))
-}
-
-fn kitty_image_data_ptr_len(
- image: ffi::GhosttyKittyGraphicsImage,
-) -> Result<(*const u8, usize), Error> {
- let mut ptr_out: *const u8 = ptr::null();
- let mut len = 0usize;
- unsafe {
- ffi::ghostty_kitty_graphics_image_get(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR,
- (&mut ptr_out as *mut *const u8).cast(),
- )
- .into_result()?;
- ffi::ghostty_kitty_graphics_image_get(
- image,
- ffi::GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN,
- (&mut len as *mut usize).cast(),
- )
- .into_result()?;
- }
- Ok((ptr_out, len))
-}
-
-fn kitty_image_data_from_ptr(ptr_out: *const u8, len: usize) -> Vec<u8> {
- if ptr_out.is_null() || len == 0 {
- return Vec::new();
- }
- unsafe { slice::from_raw_parts(ptr_out, len) }.to_vec()
-}
-
-// Hashes the full payload. Callers cache the result per image id and only
-// recompute it when the image's transmit time changes.
-fn kitty_image_fingerprint(
- ptr_out: *const u8,
- len: usize,
- image_width: u32,
- image_height: u32,
- format: KittyImageFormat,
-) -> u64 {
- let mut hasher = DefaultHasher::new();
- len.hash(&mut hasher);
- image_width.hash(&mut hasher);
- image_height.hash(&mut hasher);
- format.hash(&mut hasher);
- if ptr_out.is_null() || len == 0 {
- return hasher.finish();
- }
-
- let data = unsafe { slice::from_raw_parts(ptr_out, len) };
- data.hash(&mut hasher);
- hasher.finish()
-}
-
-fn grid_ref_hyperlink_uri(grid_ref: &ffi::GhosttyGridRef) -> Result<Option<String>, Error> {
- let mut required = 0usize;
- let result =
- unsafe { ffi::ghostty_grid_ref_hyperlink_uri(grid_ref, ptr::null_mut(), 0, &mut required) };
- if result != ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE {
- result.into_result()?;
- }
- if required == 0 {
- return Ok(None);
- }
- let mut buffer = vec![0u8; required];
- unsafe {
- ffi::ghostty_grid_ref_hyperlink_uri(
- grid_ref,
- buffer.as_mut_ptr(),
- buffer.len(),
- &mut required,
- )
- .into_result()?;
- }
- buffer.truncate(required);
- Ok(Some(String::from_utf8_lossy(&buffer).into_owned()))
-}
-
-pub struct RenderState {
- raw: ffi::GhosttyRenderState,
-}
-
-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(&self) -> Result<RenderCursor, Error> {
- let mut cursor = ffi::GhosttyRenderStateCursor {
- size: mem::size_of::<ffi::GhosttyRenderStateCursor>(),
- ..Default::default()
- };
- unsafe {
- ffi::ghostty_render_state_get(
- self.raw,
- ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR,
- (&mut cursor as *mut ffi::GhosttyRenderStateCursor).cast(),
- )
- .into_result()?;
- }
- Ok(RenderCursor {
- // Upstream leaves these fields undefined when the cursor is offscreen.
- viewport: if cursor.viewport_has_value {
- Some(CursorViewport {
- x: cursor.viewport_x,
- y: cursor.viewport_y,
- wide_tail: cursor.wide_tail,
- })
- } else {
- None
- },
- visible: cursor.visible,
- blinking: cursor.blinking,
- visual_style: CursorVisualStyle::from_raw(cursor.visual_style),
- })
- }
-
- 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_get(
- self.raw,
- ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_COLORS,
- (&mut colors as *mut ffi::GhosttyRenderStateColors).cast(),
- )
- .into_result()?;
- }
- Ok(RenderColors {
- background: colors.background.into(),
- foreground: colors.foreground.into(),
- palette: colors.palette.map(Into::into),
- })
- }
-
- pub fn clean(&mut self) -> Result<(), Error> {
- unsafe { ffi::ghostty_render_state_clean(self.raw).into_result() }
- }
-
- 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).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)
- }
-}
-
-// 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,
-}
-
-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: ffi::GhosttyKey) {
- 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,
-}
-
-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)
- })
- }
-}
-
-// SAFETY: the opaque encoder handle is only used behind external synchronization in pane runtime.
-unsafe impl Send for KeyEncoder {}
-
-impl Drop for KeyEncoder {
- fn drop(&mut self) {
- unsafe { ffi::ghostty_key_encoder_free(self.raw) }
- }
-}
-
-pub struct MouseEvent {
- raw: ffi::GhosttyMouseEvent,
-}
-
-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,
-}
-
-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 set_format(&mut self, format: ffi::GhosttyMouseFormat) {
- unsafe {
- ffi::ghostty_mouse_encoder_setopt(
- self.raw,
- ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_FORMAT,
- (&format as *const ffi::GhosttyMouseFormat).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,
-}
-
-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 next_dirty(&mut self) -> Option<u16> {
- let mut y = 0u16;
- // SAFETY: iterator is live and y matches the viewport-coordinate output type.
- let advanced =
- unsafe { ffi::ghostty_render_state_row_iterator_next_dirty(self.iterator.raw, &mut y) };
- advanced.then_some(y)
- }
-
- 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)
- }
-
- #[cfg(windows)]
- pub fn wrap_state(&self) -> Result<(bool, bool), Error> {
- let mut row = 0;
- // SAFETY: row output matches requested row data type.
- unsafe {
- ffi::ghostty_render_state_row_get(
- self.iterator.raw,
- ffi::GhosttyRenderStateRowData_GHOSTTY_RENDER_STATE_ROW_DATA_RAW,
- (&mut row as *mut ffi::GhosttyRow).cast(),
- )
- .into_result()?;
- }
- let mut soft_wrapped = false;
- // SAFETY: wrap output matches requested row data type.
- unsafe {
- ffi::ghostty_row_get(
- row,
- ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP,
- (&mut soft_wrapped as *mut bool).cast(),
- )
- .into_result()?;
- }
- let mut wrap_continuation = false;
- // SAFETY: wrap continuation output matches requested row data type.
- unsafe {
- ffi::ghostty_row_get(
- row,
- ffi::GhosttyRowData_GHOSTTY_ROW_DATA_WRAP_CONTINUATION,
- (&mut wrap_continuation as *mut bool).cast(),
- )
- .into_result()?;
- }
- Ok((soft_wrapped, wrap_continuation))
- }
-
- 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,
- ) -> 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).cast(),
- )
- .into_result()?;
- }
- Ok(RowCellIter { cells })
- }
-}
-
-pub struct RowCells {
- raw: ffi::GhosttyRenderStateRowCells,
-}
-
-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,
-}
-
-#[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.
- 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() }
- }
-
- fn raw_cell(&self) -> Result<ffi::GhosttyCell, Error> {
- let mut raw = ffi::GhosttyCell::default();
- unsafe {
- ffi::ghostty_render_state_row_cells_get(
- self.cells.raw,
- ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW,
- (&mut raw as *mut ffi::GhosttyCell).cast(),
- )
- .into_result()?;
- }
- 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;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_WIDE,
- (&mut wide as *mut ffi::GhosttyCellWide).cast(),
- )
- .into_result()?;
- }
- Ok(CellWide::from_raw(wide))
- }
-
- pub fn has_hyperlink(&self) -> Result<bool, Error> {
- let raw = self.raw_cell()?;
- let mut has_hyperlink = false;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_HYPERLINK,
- (&mut has_hyperlink as *mut bool).cast(),
- )
- .into_result()?;
- }
- Ok(has_hyperlink)
- }
-
- 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 content_bg_color(&self) -> Result<Option<CellColor>, Error> {
- let raw = self.raw_cell()?;
- let mut tag = ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_CODEPOINT;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CONTENT_TAG,
- (&mut tag as *mut ffi::GhosttyCellContentTag).cast(),
- )
- .into_result()?;
- }
-
- match tag {
- ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE => {
- let mut index = 0u8;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_PALETTE,
- (&mut index as *mut u8).cast(),
- )
- .into_result()?;
- }
- Ok(Some(CellColor::Palette(index)))
- }
- ffi::GhosttyCellContentTag_GHOSTTY_CELL_CONTENT_BG_COLOR_RGB => {
- let mut color = ffi::GhosttyColorRgb::default();
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_COLOR_RGB,
- (&mut color as *mut ffi::GhosttyColorRgb).cast(),
- )
- .into_result()?;
- }
- Ok(Some(CellColor::Rgb(color.into())))
- }
- _ => Ok(None),
- }
- }
-
- 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)),
- }
- }
-
- fn raw_cell_text_into(&self, text: &mut String) -> Result<(), Error> {
- let raw = self.raw_cell()?;
- let mut has_text = false;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_HAS_TEXT,
- (&mut has_text as *mut bool).cast(),
- )
- .into_result()?;
- }
- if !has_text {
- return Ok(());
- }
-
- let mut codepoint = 0u32;
- unsafe {
- ffi::ghostty_cell_get(
- raw,
- ffi::GhosttyCellData_GHOSTTY_CELL_DATA_CODEPOINT,
- (&mut codepoint as *mut u32).cast(),
- )
- .into_result()?;
- }
- if let Some(ch) = char::from_u32(codepoint) {
- text.push(ch);
- }
- Ok(())
- }
-
- pub fn grapheme_text(&self) -> Result<String, Error> {
- let mut bytes = Vec::new();
- let mut text = String::new();
- self.grapheme_text_into(&mut bytes, &mut text)?;
- Ok(text)
- }
-
- pub fn grapheme_text_into(&self, bytes: &mut Vec<u8>, text: &mut String) -> Result<(), Error> {
- text.clear();
- bytes.clear();
-
- let mut buffer = ffi::GhosttyBuffer {
- ptr: ptr::null_mut(),
- cap: 0,
- len: 0,
- };
- let result = unsafe {
- ffi::ghostty_render_state_row_cells_get(
- self.cells.raw,
- ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8,
- (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
- )
- };
- match result {
- ffi::GhosttyResult_GHOSTTY_SUCCESS if buffer.len == 0 => {
- return self.raw_cell_text_into(text);
- }
- ffi::GhosttyResult_GHOSTTY_SUCCESS => {
- return Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE));
- }
- ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE => {}
- other => return Err(Error(other)),
- }
-
- if buffer.len == 0 {
- return self.raw_cell_text_into(text);
- }
- bytes.resize(buffer.len, 0);
- let mut buffer = ffi::GhosttyBuffer {
- ptr: bytes.as_mut_ptr(),
- cap: bytes.len(),
- len: 0,
- };
- unsafe {
- ffi::ghostty_render_state_row_cells_get(
- self.cells.raw,
- ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8,
- (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
- )
- .into_result()?;
- }
- if buffer.len > bytes.len() {
- return Err(Error(ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE));
- }
- bytes.truncate(buffer.len);
- match std::str::from_utf8(bytes) {
- Ok(value) => text.push_str(value),
- Err(_) => text.push_str(&String::from_utf8_lossy(bytes)),
- }
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::sync::Arc;
-
- fn write_numbered_lines(terminal: &mut Terminal, count: usize) {
- for i in 0..count {
- terminal.write(format!("{i:06}\r\n").as_bytes());
- }
- }
-
- fn write_padded_lines(terminal: &mut Terminal, count: usize, width: usize) {
- let line = format!("{}\r\n", "x".repeat(width));
- terminal.write(line.repeat(count).as_bytes());
- }
-
- fn first_rendered_row_text(terminal: &Terminal) -> String {
- 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();
- let mut row_cells = RowCells::new().unwrap();
- let mut bytes = Vec::new();
- let mut cell_text = String::new();
- let mut row_text = String::new();
-
- assert!(rows.next());
- let mut cells = rows.populate_cells(&mut row_cells).unwrap();
- while cells.next() {
- cells
- .grapheme_text_into(&mut bytes, &mut cell_text)
- .unwrap();
- row_text.push_str(&cell_text);
- }
- row_text.trim_end().to_owned()
- }
-
- fn build_info_bool(data: ffi::GhosttyBuildInfo) -> bool {
- let mut out = false;
- unsafe {
- ffi::ghostty_build_info(data, (&mut out as *mut bool).cast())
- .into_result()
- .unwrap();
- }
- out
- }
-
- fn build_info_optimize() -> ffi::GhosttyOptimizeMode {
- let mut out = ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_DEBUG;
- unsafe {
- ffi::ghostty_build_info(
- ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_OPTIMIZE,
- (&mut out as *mut ffi::GhosttyOptimizeMode).cast(),
- )
- .into_result()
- .unwrap();
- }
- out
- }
-
- #[test]
- fn incremental_compression_preserves_cold_scrollback() {
- let mut terminal = Terminal::new(80, 24, 20_000_000).unwrap();
- let initial_activity = terminal.compression_activity().unwrap();
- let suffix = "x".repeat(66);
- for line in 1..=10_000 {
- terminal.write(format!("{line:05} {suffix}\r\n").as_bytes());
- }
- assert_ne!(terminal.compression_activity().unwrap(), initial_activity);
-
- let mut complete = false;
- for _ in 0..10_000 {
- match terminal.compress_incremental().unwrap() {
- TerminalCompressionResult::Unsupported => return,
- TerminalCompressionResult::Pending => {}
- TerminalCompressionResult::Complete => {
- complete = true;
- break;
- }
- }
- }
- assert!(complete, "incremental compression did not converge");
-
- let oldest = terminal.read_text_screen((0, 0), (79, 0), false).unwrap();
- assert!(oldest.starts_with("00001 "));
- let last_row = terminal.total_rows().unwrap() as u32 - 1;
- let newest = terminal
- .read_text_screen((0, last_row - 1), (79, last_row), false)
- .unwrap();
- assert!(newest.contains("10000"));
- }
-
- #[test]
- fn kitty_png_replacement_rejects_invalid_payload_without_placing_it() {
- use base64::Engine as _;
- let mut png = Vec::new();
- {
- let mut encoder = png::Encoder::new(&mut png, 2, 1);
- encoder.set_color(png::ColorType::Rgba);
- encoder.set_depth(png::BitDepth::Eight);
- let mut writer = encoder.write_header().unwrap();
- writer
- .write_image_data(&[255, 0, 0, 255, 0, 0, 255, 255])
- .unwrap();
- }
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- let upload = |payload: &[u8]| {
- format!(
- "\x1b[H\x1b_Ga=T,f=100,i=7,p=3,c=2,r=1,q=2;{}\x1b\\",
- base64::engine::general_purpose::STANDARD.encode(payload)
- )
- };
- terminal.write(upload(&png).as_bytes());
- let before = terminal.kitty_image_placements().unwrap();
- assert_eq!(before.len(), 1);
- assert_eq!((before[0].image_width, before[0].image_height), (2, 1));
- assert_eq!(before[0].data, [255, 0, 0, 255, 0, 0, 255, 255]);
- // The parser retires the old placement, but never places corrupt data.
- terminal.write(upload(&png[..png.len() / 2]).as_bytes());
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
- }
-
- #[cfg(unix)]
- #[test]
- fn kitty_file_image_survives_source_mutation_and_unlink() {
- use base64::Engine as _;
- let path = std::path::PathBuf::from(format!(
- "/var/tmp/herdr-kitty-source-{}-{}",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- std::fs::write(&path, [255, 0, 0, 255]).unwrap();
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- terminal.write(
- format!(
- "\x1b_Ga=T,f=32,t=f,i=7,p=3,s=1,v=1,c=1,r=1,q=2;{}\x1b\\",
- base64::engine::general_purpose::STANDARD.encode(path.to_str().unwrap())
- )
- .as_bytes(),
- );
- std::fs::write(&path, [0, 0, 255, 255]).unwrap();
- std::fs::remove_file(&path).unwrap();
- let placements = terminal.kitty_image_placements().unwrap();
- assert_eq!(placements.len(), 1);
- assert_eq!(placements[0].data, [255, 0, 0, 255]);
- }
-
- #[test]
- fn kitty_image_fingerprint_covers_full_payload() {
- let mut data = vec![1u8; 4096 * 4];
- let original =
- kitty_image_fingerprint(data.as_ptr(), data.len(), 100, 50, KittyImageFormat::Png);
-
- data[4096 + 123] = 2;
- let changed_outside_sampled_windows =
- kitty_image_fingerprint(data.as_ptr(), data.len(), 100, 50, KittyImageFormat::Png);
- assert_ne!(original, changed_outside_sampled_windows);
- }
-
- #[test]
- fn kitty_image_fingerprint_refreshes_on_retransmission() {
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.write(b"\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=10,r=5,q=2;/wAA/w==\x1b\\");
- let first = terminal
- .kitty_image_placements_with_data_filter(|_| true)
- .unwrap();
- assert_eq!(first.len(), 1);
- let first_generation = terminal
- .kitty_fingerprints
- .lock()
- .unwrap()
- .get(&7)
- .unwrap()
- .generation;
- assert_ne!(first_generation, 0);
-
- // Retransmission removes old placements; transmit-and-display replaces it.
- terminal.write(b"\x1b[H\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=10,r=5,q=2;AAAAAA==\x1b\\");
- let second = terminal
- .kitty_image_placements_with_data_filter(|_| true)
- .unwrap();
- assert_eq!(second.len(), 1);
- assert_ne!(first[0].data_fingerprint, second[0].data_fingerprint);
- let second_generation = terminal
- .kitty_fingerprints
- .lock()
- .unwrap()
- .get(&7)
- .unwrap()
- .generation;
- assert_ne!(first_generation, second_generation);
-
- // No retransmission, so the fingerprint and generation stay stable.
- let third = terminal
- .kitty_image_placements_with_data_filter(|_| true)
- .unwrap();
- assert_eq!(second[0].data_fingerprint, third[0].data_fingerprint);
- assert_eq!(
- terminal
- .kitty_fingerprints
- .lock()
- .unwrap()
- .get(&7)
- .unwrap()
- .generation,
- second_generation
- );
- }
-
- #[test]
- fn kitty_storage_generation_skips_only_proven_empty_storage() {
- let mut terminal = Terminal::new(10, 5, 1_000_000).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- terminal.resize(10, 5, 8, 16).unwrap();
-
- assert_eq!(terminal.kitty_graphics_generation().unwrap(), 0);
- assert!(!terminal.kitty_graphics_may_have_placements().unwrap());
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
-
- terminal.write(b"\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\");
- let transmitted = terminal.kitty_graphics_generation().unwrap();
- assert_ne!(transmitted, 0);
- assert!(terminal.kitty_graphics_may_have_placements().unwrap());
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
- assert_eq!(terminal.kitty_empty_generation.get(), Some(transmitted));
- assert!(!terminal.kitty_graphics_may_have_placements().unwrap());
-
- terminal.write(b"plain text");
- assert_eq!(terminal.kitty_graphics_generation().unwrap(), transmitted);
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
-
- terminal.write(b"\x1b_Ga=p,i=1,p=1,c=1,r=1;\x1b\\");
- let placed = terminal.kitty_graphics_generation().unwrap();
- assert_ne!(placed, transmitted);
- assert!(terminal.kitty_graphics_may_have_placements().unwrap());
- assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);
-
- terminal.resize(10, 5, 12, 24).unwrap();
- assert_eq!(terminal.kitty_graphics_generation().unwrap(), placed);
- assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);
-
- write_numbered_lines(&mut terminal, 20);
- assert_eq!(terminal.kitty_graphics_generation().unwrap(), placed);
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
- assert_ne!(terminal.kitty_empty_generation.get(), Some(placed));
- assert!(terminal.kitty_graphics_may_have_placements().unwrap());
- terminal.scroll_viewport_row(0);
- assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);
-
- // Delete-visible must preserve images in scrollback, even while viewed.
- terminal.write(b"\x1b_Ga=d,d=A\x1b\\");
- assert_eq!(terminal.kitty_graphics_generation().unwrap(), placed);
- assert_eq!(terminal.kitty_image_placements().unwrap().len(), 1);
- terminal.write(b"\x1b_Ga=d,d=I,i=1\x1b\\");
- let deleted = terminal.kitty_graphics_generation().unwrap();
- assert_ne!(deleted, placed);
- assert!(terminal.kitty_graphics_may_have_placements().unwrap());
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
- assert_eq!(terminal.kitty_empty_generation.get(), Some(deleted));
- assert!(!terminal.kitty_graphics_may_have_placements().unwrap());
- }
-
- #[test]
- fn build_info_contract_matches_expected_vendored_features() {
- let _simd = build_info_bool(ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_SIMD);
- let _tmux_control_mode =
- build_info_bool(ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE);
- let _kitty_graphics =
- build_info_bool(ffi::GhosttyBuildInfo_GHOSTTY_BUILD_INFO_KITTY_GRAPHICS);
-
- let optimize = build_info_optimize();
- assert!(matches!(
- optimize,
- ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_DEBUG
- | ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_RELEASE_SAFE
- | ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_RELEASE_SMALL
- | ffi::GhosttyOptimizeMode_GHOSTTY_OPTIMIZE_RELEASE_FAST
- ));
- }
-
- #[test]
- fn kitty_graphics_direct_rgba_placement_is_queryable() {
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- terminal.resize(10, 5, 8, 16).unwrap();
- terminal.write(b"\x1b_Ga=T,f=32,t=d,i=7,p=3,s=1,v=1,c=10,r=5,q=2;/wAA/w==\x1b\\");
-
- let placements = terminal.kitty_image_placements().unwrap();
- assert_eq!(placements.len(), 1);
- assert_eq!(placements[0].image_id, 7);
- assert_eq!(placements[0].placement_id, 3);
- assert_eq!(placements[0].image_width, 1);
- assert_eq!(placements[0].image_height, 1);
- assert_eq!(placements[0].format, KittyImageFormat::Rgba);
- assert_eq!(placements[0].data, [255, 0, 0, 255]);
- assert_eq!(placements[0].render.grid_cols, 10);
- assert_eq!(placements[0].render.grid_rows, 5);
- }
-
- #[test]
- fn kitty_graphics_local_media_are_enabled() {
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
-
- assert!(terminal
- .get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE)
- .unwrap());
- let mut temp_dir = ffi::GhosttyString::default();
- unsafe {
- ffi::ghostty_terminal_get(
- terminal.raw,
- ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE,
- (&mut temp_dir as *mut ffi::GhosttyString).cast(),
- )
- .into_result()
- .unwrap();
- assert_eq!(
- borrowed_bytes(temp_dir).unwrap(),
- std::env::temp_dir().to_string_lossy().as_bytes()
- );
- }
- assert!(terminal
- .get_bool(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM)
- .unwrap());
- }
-
- #[test]
- fn kitty_graphics_file_medium_rgba_placement_is_queryable() {
- use base64::Engine;
-
- let dir = std::env::temp_dir().join(format!(
- "herdr-kitty-file-medium-test-{}",
- std::process::id()
- ));
- std::fs::create_dir_all(&dir).unwrap();
- let path = dir.join("pixel.rgba");
- std::fs::write(&path, [255, 0, 0, 255]).unwrap();
-
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- terminal.resize(10, 5, 8, 16).unwrap();
- let encoded_path =
- base64::engine::general_purpose::STANDARD.encode(path.as_os_str().as_encoded_bytes());
- let command =
- format!("\x1b_Ga=T,f=32,t=f,i=9,p=4,s=1,v=1,c=10,r=5,q=2;{encoded_path}\x1b\\");
- terminal.write(command.as_bytes());
- terminal.write(b"\x1b_Ga=p,U=1,i=9,c=10,r=5\x1b\\");
-
- let placements = terminal.kitty_image_placements().unwrap();
- assert_eq!(placements.len(), 1);
- assert_eq!(placements[0].image_id, 9);
- assert_eq!(placements[0].placement_id, 4);
- assert_eq!(placements[0].image_width, 1);
- assert_eq!(placements[0].image_height, 1);
- assert_eq!(placements[0].format, KittyImageFormat::Rgba);
- assert_eq!(placements[0].data, [255, 0, 0, 255]);
- assert_eq!(placements[0].render.grid_cols, 10);
- assert_eq!(placements[0].render.grid_rows, 5);
-
- let _ = std::fs::remove_dir_all(dir);
- }
-
- #[cfg(unix)]
- #[test]
- fn kitty_graphics_file_upload_can_be_placed_later() {
- let dir = std::env::temp_dir().join(format!(
- "herdr-kitty-file-upload-test-{}",
- std::process::id()
- ));
- std::fs::create_dir_all(&dir).unwrap();
- let path = dir.join("pixel.rgba");
- std::fs::write(&path, [255, 0, 0, 255]).unwrap();
-
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- terminal.resize(10, 5, 8, 16).unwrap();
- let mut upload = Vec::new();
- crate::kitty_graphics::encode_kitty_regular_file(
- &mut upload,
- &[],
- "a=t,f=32,s=1,v=1,i=10,q=0",
- path.to_str().unwrap(),
- );
- terminal.write(&upload);
- assert!(terminal.kitty_image_placements().unwrap().is_empty());
-
- terminal.write(b"\x1b_Ga=p,i=10,p=5,c=10,r=5,C=1,q=2\x1b\\");
- let placements = terminal.kitty_image_placements().unwrap();
- assert_eq!(placements.len(), 1);
- assert_eq!(placements[0].image_id, 10);
- assert_eq!(placements[0].placement_id, 5);
- assert_eq!(placements[0].image_width, 1);
- assert_eq!(placements[0].image_height, 1);
- assert_eq!(placements[0].format, KittyImageFormat::Rgba);
- assert_eq!(placements[0].data, [255, 0, 0, 255]);
-
- let _ = std::fs::remove_dir_all(dir);
- }
-
- #[test]
- fn kitty_graphics_unicode_placeholder_placement_is_queryable() {
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.enable_kitty_graphics().unwrap();
- terminal.resize(10, 5, 8, 16).unwrap();
- terminal.write(b"\x1b_Gq=2,a=t,t=d,f=32,s=1,v=1,i=1193046,m=0;/wAA/w==\x1b\\");
- terminal.write(b"\x1b_Gq=2,a=p,U=1,i=1193046,c=2,r=1\x1b\\");
- terminal.write("\x1b[2;3H\x1b[38;2;18;52;86m\u{10eeee}\u{0305}\u{0305}\u{10eeee}\u{0305}\u{030d}\x1b[0m".as_bytes());
-
- let placements = terminal.kitty_image_placements().unwrap();
- assert_eq!(placements.len(), 1);
- assert_eq!(placements[0].image_id, 1193046);
- assert_ne!(placements[0].placement_id, 0);
- assert_eq!(placements[0].image_width, 1);
- assert_eq!(placements[0].image_height, 1);
- assert_eq!(placements[0].format, KittyImageFormat::Rgba);
- assert_eq!(placements[0].data, [255, 0, 0, 255]);
- assert_eq!(placements[0].render.viewport_col, 2);
- assert_eq!(placements[0].render.viewport_row, 1);
- assert_eq!(placements[0].render.grid_cols, 2);
- assert_eq!(placements[0].render.grid_rows, 1);
- }
-
- #[test]
- fn unicode_width_helpers_match_terminal_layout_rules() {
- assert_eq!(unicode_codepoint_width('A' as u32), 1);
- assert_eq!(unicode_codepoint_width('\u{301}' as u32), 0);
- assert_eq!(unicode_codepoint_width('η•Œ' as u32), 2);
- assert_eq!(unicode_codepoint_width(0x11_0000), 1);
-
- let cases: &[(&[u32], usize, u8)] = &[
- (&[], 0, 0),
- (&['e' as u32, '\u{301}' as u32], 2, 1),
- (&['⚠' as u32, '\u{fe0f}' as u32], 2, 2),
- (&['⚠' as u32, '\u{fe0e}' as u32], 2, 1),
- (&['πŸ‡§' as u32, 'πŸ‡·' as u32], 2, 2),
- (&['πŸ‘' as u32, '🏽' as u32], 2, 2),
- (
- &[
- 'πŸ‘¨' as u32,
- '\u{200d}' as u32,
- 'πŸ‘©' as u32,
- '\u{200d}' as u32,
- 'πŸ‘§' as u32,
- ],
- 5,
- 2,
- ),
- (&[0x11_0000, 'A' as u32], 1, 1),
- ];
- for &(codepoints, consumed, width) in cases {
- assert_eq!(unicode_grapheme_width(codepoints), (consumed, width));
- }
- }
-
- #[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 terminal_callbacks_report_pty_responses_and_pwd_changes() {
- 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\x1b]7;file:///tmp/herdr\x07");
-
- let output = responses.lock().unwrap().clone();
- assert!(!output.is_empty());
- assert!(String::from_utf8_lossy(&output).contains("R"));
- assert_eq!(terminal.take_pwd_changes(), [b"file:///tmp/herdr".to_vec()]);
- }
-
- #[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 modify_other_keys_query_tracks_mode_two() {
- let mut terminal = Terminal::new(80, 24, 0).unwrap();
-
- assert!(!terminal.modify_other_keys_enabled().unwrap());
- terminal.write(b"\x1b[>4;2m");
- assert!(terminal.modify_other_keys_enabled().unwrap());
- terminal.write(b"\x1b[>4;0m");
- assert!(!terminal.modify_other_keys_enabled().unwrap());
- }
-
- #[test]
- fn link_target_includes_offscreen_wrapped_rows() {
- let url = "https://example.com/abcdefghijklmnopqrstuv";
- let mut terminal = Terminal::new(20, 2, 1024 * 1024).unwrap();
- terminal.write(url.as_bytes());
- let expected = LinkTarget::Text {
- text: url.to_owned(),
- clicked_byte: 20,
- };
- assert_eq!(terminal.viewport_link_target(0, 0).unwrap(), Some(expected));
- terminal.scroll_viewport_row(0);
- assert_eq!(
- terminal.viewport_link_target(0, 0).unwrap(),
- Some(LinkTarget::Text {
- text: url.to_owned(),
- clicked_byte: 0
- })
- );
- }
-
- #[test]
- fn link_target_prefers_explicit_uri_and_does_not_join_hard_lines() {
- let mut terminal = Terminal::new(20, 4, 1024).unwrap();
- terminal.write(b"\x1b]8;;https://example.com/real\x1b\\label\x1b]8;;\x1b\\\r\nhttps://example.com/\r\ntail");
- assert_eq!(
- terminal.viewport_link_target(2, 0).unwrap(),
- Some(LinkTarget::Uri("https://example.com/real".into()))
- );
- assert_eq!(
- terminal.viewport_link_target(1, 2).unwrap(),
- Some(LinkTarget::Text {
- text: "tail".into(),
- clicked_byte: 1
- })
- );
- }
-
- #[test]
- fn link_target_stops_at_hard_line_when_clicking_last_column() {
- let mut terminal = Terminal::new(20, 3, 1024).unwrap();
- terminal.write(b"https://example.com/x\r\nnot-part-of-url");
- assert_eq!(
- terminal.viewport_link_target(19, 0).unwrap(),
- Some(LinkTarget::Text {
- text: "https://example.com/x".into(),
- clicked_byte: 19
- })
- );
- }
-
- #[test]
- fn link_target_wide_explicit_label_works_on_both_cell_halves() {
- let mut terminal = Terminal::new(20, 2, 1024).unwrap();
- terminal.write("\x1b]8;;https://example.com/\x1b\\θ·―εΎ„\x1b]8;;\x1b\\".as_bytes());
- for col in 0..4 {
- assert_eq!(
- terminal.viewport_link_target(col, 0).unwrap(),
- Some(LinkTarget::Uri("https://example.com/".into()))
- );
- }
- }
-
- #[test]
- fn link_target_selection_budget_is_shared_across_both_directions() {
- let mut terminal = Terminal::new(20, 2, 1024).unwrap();
- terminal.write(b" abc ");
- let boundaries = [u32::from(b' ')];
- let options = ffi::GhosttyTerminalSelectWordOptions {
- size: mem::size_of::<ffi::GhosttyTerminalSelectWordOptions>(),
- ref_: terminal.grid_ref(ghostty_viewport_point(2, 0)).unwrap(),
- boundary_codepoints: boundaries.as_ptr(),
- boundary_codepoints_len: boundaries.len(),
- };
- for budget in 0..=5 {
- let mut selection = ffi::GhosttySelection::default();
- let result = unsafe {
- ffi::ghostty_terminal_select_word_bounded(
- terminal.raw,
- &options,
- budget,
- &mut selection,
- )
- };
- if budget < 5 {
- assert_eq!(
- result,
- ffi::GhosttyResult_GHOSTTY_NO_VALUE,
- "budget={budget}"
- );
- } else {
- result.into_result().unwrap();
- assert_eq!(
- terminal
- .format_selection(&selection, FormatterFormat::Plain, true, false)
- .unwrap(),
- "abc"
- );
- }
- }
- }
-
- #[test]
- fn link_target_rejects_oversized_token_without_returning_a_prefix() {
- let mut terminal = Terminal::new(100, 4, 16 * 1024 * 1024).unwrap();
- let text = format!("https://example.com/{}", "a".repeat(1_000_000));
- terminal.write(text.as_bytes());
- let started = std::time::Instant::now();
- let target = terminal.viewport_link_target(0, 0).unwrap();
- eprintln!("million-byte token lookup: {:?}", started.elapsed());
- assert!(target.is_none());
- }
-
- #[test]
- fn terminal_read_text_viewport_unwraps_soft_wrapped_selection() {
- let mut terminal = Terminal::new(5, 3, 0).unwrap();
- terminal.write("1ABCD2EFGH3IJKL".as_bytes());
-
- let text = terminal.read_text_viewport((0, 1), (2, 2), false).unwrap();
- assert_eq!(text, "2EFGH3IJ");
- }
-
- #[test]
- fn terminal_extracts_viewport_hyperlink_uri() {
- let mut terminal = Terminal::new(20, 3, 0).unwrap();
- terminal.write(b"\x1b]8;;https://example.com\x1b\\Link\x1b]8;;\x1b\\");
-
- assert_eq!(
- terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(),
- Some("https://example.com")
- );
- assert_eq!(terminal.viewport_hyperlink_uri(4, 0).unwrap(), None);
- }
-
- #[test]
- fn terminal_read_text_viewport_handles_wide_chars() {
- let mut terminal = Terminal::new(5, 3, 0).unwrap();
- terminal.write("1A⚑".as_bytes());
-
- let full = terminal.read_text_viewport((0, 0), (3, 0), false).unwrap();
- assert_eq!(full, "1A⚑");
-
- let through_wide_head = terminal.read_text_viewport((0, 0), (2, 0), false).unwrap();
- assert_eq!(through_wide_head, "1A⚑");
-
- let wide_only = terminal.read_text_viewport((3, 0), (3, 0), false).unwrap();
- assert_eq!(wide_only, "⚑");
- }
-
- #[test]
- fn zero_max_scrollback_disables_history() {
- let mut terminal = Terminal::new(80, 3, 0).unwrap();
- write_numbered_lines(&mut terminal, 3000);
- assert_eq!(terminal.scrollback_rows().unwrap(), 0);
- }
-
- #[test]
- fn max_scrollback_limit_bytes_retains_more_history_for_larger_limits() {
- let mut small = Terminal::new(80, 3, 1_000_000).unwrap();
- let mut large = Terminal::new(80, 3, 10_000_000).unwrap();
-
- write_padded_lines(&mut small, 1_250, 70);
- write_padded_lines(&mut large, 1_250, 70);
-
- let small_scrollback = small.scrollback_rows().unwrap();
- let large_scrollback = large.scrollback_rows().unwrap();
-
- assert!(
- large_scrollback > small_scrollback,
- "expected larger byte limit to retain more history, got small={small_scrollback}, large={large_scrollback}"
- );
- }
-
- #[test]
- fn large_negative_scroll_delta_reaches_top_of_scrollback() {
- let mut terminal = Terminal::new(80, 3, 1_000_000).unwrap();
- write_numbered_lines(&mut terminal, 1000);
-
- let before = terminal.scrollbar().unwrap();
- assert!(before.total > before.len);
-
- terminal.scroll_viewport_bottom();
- terminal.scroll_viewport_delta(-10_000);
-
- let after = terminal.scrollbar().unwrap();
- assert_eq!(after.offset, 0);
- assert_eq!(after.len, before.len);
- }
-
- #[test]
- fn absolute_scroll_row_round_trips_and_clamps() {
- let mut terminal = Terminal::new(80, 3, 1_000_000).unwrap();
- write_numbered_lines(&mut terminal, 1000);
-
- let before = terminal.scrollbar().unwrap();
- let max_row = before.total.saturating_sub(before.len);
- assert!(max_row > 0);
-
- for row in [0, max_row / 2, max_row, usize::MAX] {
- terminal.scroll_viewport_row(row);
- let after = terminal.scrollbar().unwrap();
- assert_eq!(after.offset, row.min(max_row));
- assert_eq!(after.len, before.len);
- }
- }
-
- #[test]
- fn deep_scrollback_resize_preserves_unicode_and_hyperlinks() {
- use std::fmt::Write as _;
-
- let mut terminal = Terminal::new(20, 5, 100_000_000).unwrap();
- let mut input = String::from("\x1b]8;;https://example.com\x1b\\FIRST πŸ‡§πŸ‡·\x1b]8;;\x1b\\\r\n");
- for line in 0..70_000 {
- writeln!(input, "{line:05} πŸ‘¨β€πŸ‘©β€πŸ‘§").unwrap();
- }
- terminal.write(input.as_bytes());
-
- assert!(terminal.scrollback_rows().unwrap() > u16::MAX as usize);
- terminal.scroll_viewport_delta(-100_000);
- assert_eq!(terminal.scrollbar().unwrap().offset, 0);
- assert!(terminal
- .read_text_viewport((0, 0), (19, 0), false)
- .unwrap()
- .starts_with("FIRST πŸ‡§πŸ‡·"));
- assert_eq!(
- terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(),
- Some("https://example.com")
- );
-
- terminal.resize(10, 5, 8, 16).unwrap();
- terminal.scroll_viewport_delta(-100_000);
- let metrics = terminal.scrollbar().unwrap();
- assert_eq!(metrics.offset, 0);
- assert_eq!(metrics.len, 5);
- assert!(terminal
- .read_text_viewport((0, 0), (9, 0), false)
- .unwrap()
- .starts_with("FIRST πŸ‡§πŸ‡·"));
- assert_eq!(
- terminal.viewport_hyperlink_uri(0, 0).unwrap().as_deref(),
- Some("https://example.com")
- );
- }
-
- #[test]
- fn raw_resize_preserves_content_without_replaying_terminal_effects() {
- let mut terminal = Terminal::new(20, 6, 100_000).unwrap();
- terminal.write(b"header\r\n\x1b[6;1Htail\x1b[6;18H");
- for (cols, rows) in [(10, 3), (30, 8), (8, 4), (20, 6)] {
- terminal.resize(cols, rows, 8, 16).unwrap();
- terminal.write(b"X");
- let text = terminal
- .read_text_screen(
- (0, 0),
- (cols - 1, terminal.total_rows().unwrap() as u32 - 1),
- false,
- )
- .unwrap();
- assert!(
- text.contains("tail"),
- "lost tail after {cols}x{rows}: {text:?}"
- );
- assert_eq!(text.matches("tail").count(), 1);
- assert!(terminal.cursor_y().unwrap() < rows);
- }
- terminal.write(b"\x1b[2J\x1b[H");
- terminal.resize(12, 3, 8, 16).unwrap();
- assert!(terminal
- .read_text_viewport((0, 0), (11, 2), false)
- .unwrap()
- .trim()
- .is_empty());
- }
-
- #[test]
- fn clipboard_queries_never_disclose_contents_and_split_writes_complete_once() {
- for suffix in [b"\x07".as_slice(), b"\x1b\\".as_slice()] {
- let bytes = [b"\x1b]52;c;YQBi".as_slice(), suffix].concat();
- for split in 0..=bytes.len() {
- let mut terminal = Terminal::new(10, 3, 0).unwrap();
- let replies = Arc::new(Mutex::new(Vec::new()));
- let captured = Arc::clone(&replies);
- terminal
- .set_write_pty_callback(move |bytes| {
- captured.lock().unwrap().extend_from_slice(bytes)
- })
- .unwrap();
- terminal.write(&bytes[..split]);
- terminal.write(&bytes[split..]);
- assert_eq!(terminal.take_clipboard_writes(), vec![b"a\0b".to_vec()]);
- terminal.write(b"\x1b]52;c;?\x07\x1b]52;p;YQBi\x07");
- assert!(terminal.take_clipboard_writes().is_empty());
- assert!(replies.lock().unwrap().is_empty());
- }
- }
- }
-
- #[test]
- fn active_screen_and_cursor_visibility_contract() {
- let mut terminal = Terminal::new(12, 3, 0).unwrap();
- let mut render_state = RenderState::new().unwrap();
-
- terminal.write(b"primary");
- assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Primary);
- assert_eq!(
- terminal.read_text_viewport((0, 0), (6, 0), false).unwrap(),
- "primary"
- );
-
- render_state.update(&terminal).unwrap();
- assert!(render_state.cursor().unwrap().visible);
- terminal.write(b"\x1b[?25l");
- render_state.update(&terminal).unwrap();
- assert!(!render_state.cursor().unwrap().visible);
-
- terminal.write(b"\x1b[?1049h\x1b[HALT");
- assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Alternate);
- assert_eq!(
- terminal.read_text_viewport((0, 0), (2, 0), false).unwrap(),
- "ALT"
- );
-
- terminal.write(b"\x1b[?1049l");
- assert_eq!(terminal.active_screen().unwrap(), ActiveScreen::Primary);
- assert_eq!(
- terminal.read_text_viewport((0, 0), (6, 0), false).unwrap(),
- "primary"
- );
- }
-
- #[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 text = cells.grapheme_text().unwrap();
- if text.is_empty() {
- line.push(' ');
- } else {
- line.push_str(&text);
- }
- }
- 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);
- }
-
- #[test]
- fn render_cells_preserve_issue_453_unicode_payload_exactly() {
- const PAYLOAD: &str = "README πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ πŸ§‘β€πŸ’» βœ… ⚑ ζΌ’ε­— cafΓ© é πŸ³οΈβ€πŸŒˆ πŸš€";
- let mut terminal = Terminal::new(80, 3, 100).unwrap();
- assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
- terminal.write(format!("{PAYLOAD}\r\n").as_bytes());
-
- assert_eq!(first_rendered_row_text(&terminal), PAYLOAD);
- }
-
- #[test]
- fn grapheme_cluster_mode_is_default_and_survives_full_reset() {
- let mut terminal = Terminal::new(80, 3, 100).unwrap();
- assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
- terminal.mode_set(MODE_GRAPHEME_CLUSTER, false).unwrap();
- assert!(!terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
-
- terminal.write(b"\x1bc");
-
- assert!(terminal.mode_get(MODE_GRAPHEME_CLUSTER).unwrap());
- }
-
- #[test]
- fn screen_text_rows_preserve_wrap_and_grapheme_cells() {
- let mut terminal = Terminal::new(5, 3, 100).unwrap();
- terminal.write("abcdef\r\nη•Œe\u{301}".as_bytes());
-
- let rows = terminal.screen_text_rows().unwrap();
-
- assert_eq!(rows.len(), 3);
- assert!(rows[0].soft_wrapped);
- assert!(!rows[0].wrap_continuation);
- assert!(!rows[1].soft_wrapped);
- assert!(rows[1].wrap_continuation);
- assert!(!rows[2].wrap_continuation);
- assert_eq!(rows[2].cells[0].wide, CellWide::Wide);
- assert_eq!(rows[2].cells[0].graphemes, vec!['η•Œ' as u32]);
- assert_eq!(rows[2].cells[1].wide, CellWide::SpacerTail);
- assert_eq!(rows[2].cells[2].graphemes, vec!['e' as u32, 0x301]);
- }
-
- #[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());
- }
- }
- {
- let mut iterator = RowIterator::new().unwrap();
- let mut rows = render_state.populate_row_iterator(&mut iterator).unwrap();
- assert_eq!(rows.next_dirty(), Some(0));
- assert_eq!(rows.next_dirty(), Some(1));
- assert_eq!(rows.next_dirty(), Some(2));
- assert_eq!(rows.next_dirty(), None);
- }
- render_state.set_dirty(Dirty::Clean).unwrap();
- assert_eq!(render_state.dirty().unwrap(), Dirty::Clean);
- {
- let mut iterator = RowIterator::new().unwrap();
- let mut rows = render_state.populate_row_iterator(&mut iterator).unwrap();
- assert_eq!(rows.next_dirty(), None);
- }
-
- 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 let Some(y) = rows.next_dirty() {
- assert_eq!(y, 0);
- assert!(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);
- }
-
- fn test_clipboard_content(mime: &[u8], data: &[u8]) -> ffi::GhosttyClipboardContent {
- ffi::GhosttyClipboardContent {
- mime: ffi::GhosttyString {
- ptr: mime.as_ptr(),
- len: mime.len(),
- },
- data: ffi::GhosttyString {
- ptr: data.as_ptr(),
- len: data.len(),
- },
- }
- }
-
- fn invoke_clipboard_callback(
- terminal: &mut Terminal,
- contents: &[ffi::GhosttyClipboardContent],
- size: usize,
- ) -> ffi::GhosttyClipboardWriteResult {
- unsafe extern "C" fn reply(
- request: *const ffi::GhosttyClipboardWrite,
- response: *const ffi::GhosttyClipboardWriteReply,
- ) {
- let state = unsafe {
- &mut *((*request).ctx as *mut (ffi::GhosttyClipboardWriteResult, bool, usize))
- };
- state.0 = unsafe { (*response).result };
- state.1 = unsafe { (*response).remember };
- state.2 += 1;
- }
- let mut response = (
- ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA,
- false,
- 0usize,
- );
- let request = ffi::GhosttyClipboardWrite {
- size,
- location: ffi::GhosttyClipboardLocation_GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
- contents: contents.as_ptr(),
- contents_len: contents.len(),
- ctx: (&mut response as *mut (ffi::GhosttyClipboardWriteResult, bool, usize)).cast(),
- reply: Some(reply),
- ..Default::default()
- };
- // SAFETY: the request, reply state and borrowed content live through this call.
- unsafe {
- clipboard_write_trampoline(
- terminal.raw,
- (&mut *terminal.callback_state as *mut TerminalCallbackState).cast(),
- &request,
- );
- }
- assert_eq!(
- response.2,
- usize::from(size >= mem::size_of::<ffi::GhosttyClipboardWrite>())
- );
- assert!(!response.1, "migration must not create clipboard grants");
- response.0
- }
-
- #[test]
- fn clipboard_callback_ignores_clear_and_rejects_unsupported_writes() {
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- let full_size = std::mem::size_of::<ffi::GhosttyClipboardWrite>();
- let success = ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS;
- let unsupported =
- ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED;
- let invalid = ffi::GhosttyClipboardWriteResult_GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA;
-
- assert_eq!(
- invoke_clipboard_callback(&mut terminal, &[], full_size),
- success
- );
- assert!(terminal.take_clipboard_writes().is_empty());
-
- let empty = test_clipboard_content(b"text/plain", b"");
- assert_eq!(
- invoke_clipboard_callback(&mut terminal, &[empty], full_size),
- unsupported
- );
- let text = test_clipboard_content(b"text/plain", b"text");
- let image = test_clipboard_content(b"image/png", b"image");
- assert_eq!(
- invoke_clipboard_callback(&mut terminal, &[text, image], full_size),
- unsupported
- );
-
- let oversized = vec![b'x'; MAX_CLIPBOARD_BYTES + 1];
- let oversized = test_clipboard_content(b"text/plain", &oversized);
- assert_eq!(
- invoke_clipboard_callback(&mut terminal, &[oversized], full_size),
- invalid
- );
- assert_eq!(
- invoke_clipboard_callback(&mut terminal, &[text], full_size - 1),
- invalid
- );
- assert!(terminal.take_clipboard_writes().is_empty());
- }
-
- #[test]
- fn libghostty_completes_osc52_writes_for_bel_and_st_without_queries() {
- let mut terminal = Terminal::new(10, 5, 0).unwrap();
- terminal.write(b"\x1b]52;c;aGVs");
- assert!(terminal.take_clipboard_writes().is_empty());
- terminal.write(b"bG8=\x07");
- assert_eq!(terminal.take_clipboard_writes(), vec![b"hello".to_vec()]);
-
- terminal.write(b"\x1b]52;c;d29ybGQ=\x1b\\");
- assert_eq!(terminal.take_clipboard_writes(), vec![b"world".to_vec()]);
-
- terminal.write(b"\x1b]52;c;?\x07");
- assert!(terminal.take_clipboard_writes().is_empty());
-
- terminal.write(b"\x1b]52;c;\x07");
- assert!(terminal.take_clipboard_writes().is_empty());
- }
-
- #[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);
- }
-}