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.rs241
1 files changed, 210 insertions, 31 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
index 82714118..43a70fb1 100644
--- a/src/ghostty/mod.rs
+++ b/src/ghostty/mod.rs
@@ -22,7 +22,10 @@ use std::ops::RangeInclusive;
use std::os::raw::c_char;
use std::ptr;
use std::slice;
-use std::sync::{Mutex, Once, OnceLock};
+use std::sync::{Arc, Mutex, Once, OnceLock};
+
+use crate::pane_graphics_files::OwnedExport;
+mod native_source;
pub use bindings as ffi;
@@ -227,6 +230,7 @@ pub struct KittyImagePlacement {
pub data_len: usize,
pub data_fingerprint: u64,
pub data: Vec<u8>,
+ pub(crate) source_file: Option<Arc<OwnedExport>>,
pub render: KittyPlacementRenderInfo,
}
@@ -239,8 +243,11 @@ pub struct KittyImageDescriptor {
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,
@@ -690,6 +697,16 @@ fn install_png_decoder_once() {
});
}
+#[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,
@@ -701,6 +718,8 @@ unsafe extern "C" fn decode_png_trampoline(
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;
};
@@ -820,6 +839,7 @@ pub struct Terminal {
callback_state: Box<TerminalCallbackState>,
kitty_fingerprints: Mutex<HashMap<u32, KittyImageFingerprintEntry>>,
kitty_empty_generation: Cell<Option<u64>>,
+ kitty_png_forwarding: bool,
}
impl Terminal {
@@ -845,6 +865,7 @@ impl Terminal {
}),
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;
@@ -1072,6 +1093,25 @@ impl Terminal {
)
.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(())
}
@@ -1882,6 +1922,65 @@ impl Terminal {
}
}
+ // 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,
@@ -1937,29 +2036,21 @@ impl Terminal {
iterator,
ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID,
)?;
- let (data_ptr, data_len) = kitty_image_data_ptr_len(image)?;
- let data_fingerprint = self.kitty_image_fingerprint_cached(
+ let (descriptor, data, source_file) = self.kitty_placement_payload(
image,
image_id,
- (data_ptr, data_len),
- image_width,
- image_height,
- format,
- );
- let descriptor = KittyImageDescriptor {
- image_id,
placement_id,
image_width,
image_height,
format,
+ needs_data,
+ )?;
+ let KittyImageDescriptor {
+ format,
data_len,
data_fingerprint,
- };
- let data = if needs_data(descriptor) {
- kitty_image_data_from_ptr(data_ptr, data_len)
- } else {
- Vec::new()
- };
+ ..
+ } = descriptor;
let x_offset = kitty_placement_u32(
iterator,
ffi::GhosttyKittyGraphicsPlacementData_GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET,
@@ -1985,6 +2076,7 @@ impl Terminal {
data_len,
data_fingerprint,
data,
+ source_file,
render: KittyPlacementRenderInfo {
pixel_width: raw_info.pixel_width,
pixel_height: raw_info.pixel_height,
@@ -2081,29 +2173,21 @@ impl Terminal {
continue;
};
let placement_id = run.synthetic_placement_id();
- let (data_ptr, data_len) = kitty_image_data_ptr_len(image)?;
- let data_fingerprint = self.kitty_image_fingerprint_cached(
+ let (descriptor, data, source_file) = self.kitty_placement_payload(
image,
image_id,
- (data_ptr, data_len),
- image_width,
- image_height,
- format,
- );
- let descriptor = KittyImageDescriptor {
- image_id,
placement_id,
image_width,
image_height,
format,
+ needs_data,
+ )?;
+ let KittyImageDescriptor {
+ format,
data_len,
data_fingerprint,
- };
- let data = if needs_data(descriptor) {
- kitty_image_data_from_ptr(data_ptr, data_len)
- } else {
- Vec::new()
- };
+ ..
+ } = descriptor;
placements.push(KittyImagePlacement {
image_id,
placement_id,
@@ -2116,6 +2200,7 @@ impl Terminal {
data_len,
data_fingerprint,
data,
+ source_file,
render: geometry.render,
});
}
@@ -2585,6 +2670,40 @@ fn kitty_image_compression(
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> {
@@ -3593,6 +3712,66 @@ mod tests {
}
#[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 =