diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-13 00:54:24 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-13 14:15:26 +0300 |
| commit | fb9aff54087aa8a606e9260a689986d2bd76f5fa (patch) | |
| tree | 1e0e2b6449c49795de5878ec43128b8e3b58d497 /src/ghostty | |
| parent | 83cc6ef01235f6353bbc73f49aaa73a82ecee402 (diff) | |
feat: render kitty graphics in local panes
Diffstat (limited to 'src/ghostty')
| -rw-r--r-- | src/ghostty/bindings.rs | 159 | ||||
| -rw-r--r-- | src/ghostty/mod.rs | 434 |
2 files changed, 591 insertions, 2 deletions
diff --git a/src/ghostty/bindings.rs b/src/ghostty/bindings.rs index 6e77288f..4998e2b6 100644 --- a/src/ghostty/bindings.rs +++ b/src/ghostty/bindings.rs @@ -2505,3 +2505,162 @@ unsafe extern "C" { #[doc = " Check if paste data is safe to paste into the terminal.\n\n Data is considered unsafe if it contains:\n - Newlines (`\\n`) which can inject commands\n - The bracketed paste end sequence (`\\x1b[201~`) which can be used\n to exit bracketed paste mode and inject commands\n\n This check is conservative and considers data unsafe regardless of\n current terminal state.\n\n @param data The paste data to check (must not be NULL)\n @param len The length of the data in bytes\n @return true if the data is safe to paste, false otherwise"] pub fn ghostty_paste_is_safe(data: *const ::std::os::raw::c_char, len: usize) -> bool; } + +// Manually maintained additions for the vendored libghostty-vt Kitty graphics +// C API. The rest of this file is generated by bindgen; keep these constants +// in sync with vendor/libghostty-vt/include/ghostty/vt/*.h until bindings are +// regenerated from the current vendored headers. +pub const GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT: + GhosttyTerminalOption = 15; +pub const GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE: + GhosttyTerminalOption = 16; +pub const GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE: + GhosttyTerminalOption = 17; +pub const GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM: + GhosttyTerminalOption = 18; +pub const GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES: GhosttyTerminalOption = 19; +pub const GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY: GhosttyTerminalOption = + 20; + +pub const GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS: GhosttyTerminalData = 30; + +pub type GhosttySysOption = ::std::os::raw::c_uint; +pub const GhosttySysOption_GHOSTTY_SYS_OPT_DECODE_PNG: GhosttySysOption = 1; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct GhosttyKittyGraphicsImpl { + _unused: [u8; 0], +} +pub type GhosttyKittyGraphics = *mut GhosttyKittyGraphicsImpl; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct GhosttyKittyGraphicsImageImpl { + _unused: [u8; 0], +} +pub type GhosttyKittyGraphicsImage = *const GhosttyKittyGraphicsImageImpl; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct GhosttyKittyGraphicsPlacementIteratorImpl { + _unused: [u8; 0], +} +pub type GhosttyKittyGraphicsPlacementIterator = *mut GhosttyKittyGraphicsPlacementIteratorImpl; + +pub type GhosttyKittyGraphicsData = ::std::os::raw::c_uint; +pub const GhosttyKittyGraphicsData_GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR: + GhosttyKittyGraphicsData = 1; + +pub type GhosttyKittyGraphicsPlacementData = ::std::os::raw::c_uint; +pub const GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID: + GhosttyKittyGraphicsPlacementData = 1; +pub const GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID: + GhosttyKittyGraphicsPlacementData = 2; +pub const GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET: + GhosttyKittyGraphicsPlacementData = 4; +pub const GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET: + GhosttyKittyGraphicsPlacementData = 5; +pub const GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z: + GhosttyKittyGraphicsPlacementData = 12; + +pub type GhosttyKittyGraphicsImageData = ::std::os::raw::c_uint; +pub const GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_WIDTH: + GhosttyKittyGraphicsImageData = 3; +pub const GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_HEIGHT: + GhosttyKittyGraphicsImageData = 4; +pub const GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_FORMAT: + GhosttyKittyGraphicsImageData = 5; +pub const GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION: + GhosttyKittyGraphicsImageData = 6; +pub const GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR: + GhosttyKittyGraphicsImageData = 7; +pub const GhosttyKittyGraphicsImageData_GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN: + GhosttyKittyGraphicsImageData = 8; + +pub type GhosttyKittyImageFormat = ::std::os::raw::c_uint; +pub const GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGB: GhosttyKittyImageFormat = 0; +pub const GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_RGBA: GhosttyKittyImageFormat = 1; +pub const GhosttyKittyImageFormat_GHOSTTY_KITTY_IMAGE_FORMAT_PNG: GhosttyKittyImageFormat = 2; + +pub type GhosttyKittyImageCompression = ::std::os::raw::c_uint; +pub const GhosttyKittyImageCompression_GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE: + GhosttyKittyImageCompression = 0; + +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct GhosttyKittyGraphicsPlacementRenderInfo { + pub size: usize, + 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 viewport_visible: bool, + pub source_x: u32, + pub source_y: u32, + pub source_width: u32, + pub source_height: u32, +} + +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct GhosttySysImage { + pub width: u32, + pub height: u32, + pub data: *mut u8, + pub data_len: usize, +} + +pub type GhosttySysDecodePngFn = ::std::option::Option< + unsafe extern "C" fn( + userdata: *mut ::std::os::raw::c_void, + allocator: *const GhosttyAllocator, + data: *const u8, + data_len: usize, + out: *mut GhosttySysImage, + ) -> bool, +>; + +unsafe extern "C" { + pub fn ghostty_sys_set( + option: GhosttySysOption, + value: *const ::std::os::raw::c_void, + ) -> GhosttyResult; + pub fn ghostty_kitty_graphics_get( + graphics: GhosttyKittyGraphics, + data: GhosttyKittyGraphicsData, + out: *mut ::std::os::raw::c_void, + ) -> GhosttyResult; + pub fn ghostty_kitty_graphics_image( + graphics: GhosttyKittyGraphics, + image_id: u32, + ) -> GhosttyKittyGraphicsImage; + pub fn ghostty_kitty_graphics_image_get( + image: GhosttyKittyGraphicsImage, + data: GhosttyKittyGraphicsImageData, + out: *mut ::std::os::raw::c_void, + ) -> GhosttyResult; + pub fn ghostty_kitty_graphics_placement_iterator_new( + allocator: *const GhosttyAllocator, + out_iterator: *mut GhosttyKittyGraphicsPlacementIterator, + ) -> GhosttyResult; + pub fn ghostty_kitty_graphics_placement_iterator_free( + iterator: GhosttyKittyGraphicsPlacementIterator, + ); + pub fn ghostty_kitty_graphics_placement_next( + iterator: GhosttyKittyGraphicsPlacementIterator, + ) -> bool; + pub fn ghostty_kitty_graphics_placement_get( + iterator: GhosttyKittyGraphicsPlacementIterator, + data: GhosttyKittyGraphicsPlacementData, + out: *mut ::std::os::raw::c_void, + ) -> GhosttyResult; + pub fn ghostty_kitty_graphics_placement_render_info( + iterator: GhosttyKittyGraphicsPlacementIterator, + image: GhosttyKittyGraphicsImage, + terminal: GhosttyTerminal_ptr, + out_info: *mut GhosttyKittyGraphicsPlacementRenderInfo, + ) -> GhosttyResult; +} diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index bc21ab99..d3a67540 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -17,6 +17,7 @@ use std::mem; use std::os::raw::c_char; use std::ptr; use std::slice; +use std::sync::Once; pub use bindings as ffi; @@ -142,6 +143,47 @@ pub const MODE_MOUSE_ALTERNATE_SCROLL: u16 = 1007; pub const MODE_BRACKETED_PASTE: u16 = 2004; pub const MODE_SYNCHRONIZED_OUTPUT: u16 = 2026; +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; + +static INSTALL_PNG_DECODER: Once = Once::new(); + +#[derive(Debug, Clone, Copy, 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: Vec<u8>, + pub render: KittyPlacementRenderInfo, +} + +#[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)] pub enum ActiveScreen { Primary, @@ -278,6 +320,95 @@ unsafe extern "C" fn write_pty_trampoline( (state.callback)(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(), + ); + }); +} + +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) }; + 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 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. @@ -332,9 +463,63 @@ impl Terminal { } } - pub fn resize(&mut self, cols: u16, rows: u16) -> Result<(), Error> { + pub fn resize( + &mut self, + cols: u16, + rows: u16, + cell_width_px: u32, + cell_height_px: u32, + ) -> Result<(), Error> { // SAFETY: self.raw is valid and sizes are plain values. - unsafe { ffi::ghostty_terminal_resize(self.raw, cols, rows, 0, 0).into_result() } + unsafe { + ffi::ghostty_terminal_resize(self.raw, cols, rows, cell_width_px, cell_height_px) + .into_result() + } + } + + pub fn enable_kitty_graphics(&mut self) -> Result<(), Error> { + install_png_decoder_once(); + let storage_limit = KITTY_IMAGE_STORAGE_LIMIT_BYTES; + let disable_medium = false; + 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, + (&disable_medium as *const bool).cast(), + ) + .into_result()?; + ffi::ghostty_terminal_set( + self.raw, + ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE, + (&disable_medium as *const bool).cast(), + ) + .into_result()?; + ffi::ghostty_terminal_set( + self.raw, + ffi::GhosttyTerminalOption_GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM, + (&disable_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()?; + } + Ok(()) } pub fn set_write_pty_callback<F>(&mut self, callback: F) -> Result<(), Error> @@ -651,11 +836,145 @@ impl Terminal { Ok(out) } + pub fn kitty_image_placements(&self) -> Result<Vec<KittyImagePlacement>, 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()?; + } + if graphics.is_null() { + 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(); + while unsafe { ffi::ghostty_kitty_graphics_placement_next(iterator) } { + if let Some(placement) = self.kitty_image_placement(graphics, iterator)? { + placements.push(placement); + } + } + placements.sort_by_key(|placement| placement.z); + Ok(placements) + } + + fn kitty_image_placement( + &self, + graphics: ffi::GhosttyKittyGraphics, + iterator: ffi::GhosttyKittyGraphicsPlacementIterator, + ) -> Result<Option<KittyImagePlacement>, Error> { + let image_id = kitty_placement_u32( + iterator, + ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, + )?; + 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 data = kitty_image_data(image)?; + let placement_id = kitty_placement_u32( + iterator, + ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID, + )?; + 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, + 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 raw(&self) -> ffi::GhosttyTerminal_ptr { 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 {} @@ -705,6 +1024,98 @@ fn grid_ref_graphemes(grid_ref: &ffi::GhosttyGridRef) -> Result<Vec<u32>, Error> Ok(buffer) } +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_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_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_data(image: ffi::GhosttyKittyGraphicsImage) -> Result<Vec<u8>, 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()?; + } + if ptr_out.is_null() || len == 0 { + return Ok(Vec::new()); + } + Ok(unsafe { slice::from_raw_parts(ptr_out, len) }.to_vec()) +} + fn grid_ref_hyperlink_uri(grid_ref: &ffi::GhosttyGridRef) -> Result<Option<String>, Error> { let mut required = 0usize; let result = @@ -1394,6 +1805,25 @@ mod tests { } #[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 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"); |
