libghostty_vt/kitty/graphics.rs
1//! API for inspecting images and placements stored via the
2//! [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).
3//!
4//! The central object is [`Graphics`], an opaque handle to the image storage
5//! associated with a terminal's active screen. From it you can iterate over
6//! placements and look up individual images.
7//!
8//! ## Obtaining a [`Graphics`] handle
9//!
10//! A [`Graphics`] handle is obtained from a terminal via
11//! [`Terminal::kitty_graphics`]. The handle is borrowed from the terminal and
12//! remains valid until the next mutating terminal call (e.g.
13//! [`Terminal::vt_write`] or [`Terminal::reset`]).
14//!
15//! Before images can be stored, Kitty graphics must be enabled on the
16//! terminal by setting a non-zero storage limit with
17//! [`Terminal::set_kitty_image_storage_limit`] and a PNG
18//! decoder must be installed via [`set_png_decoder`].
19//!
20//! ## Iterating placements
21//!
22//! Placements are inspected through a [`PlacementIterator`].
23//! The typical workflow is:
24//! 1. Create an iterator with [`PlacementIterator::new`].
25//! 2. Populate it from the storage with [`PlacementIterator::update`],
26//! returning a [`PlacementIteration`] object.
27//! 3. Optionally filter by z-layer with [`PlacementIteration::set_layer`].
28//! 4. Advance with [`PlacementIteration::next`] and read
29//! per-placement data with various methods on [`PlacementIteration`],
30//! such as [`PlacementIteration::image_id`].
31//! 5. For each placement, look up its image with [`Graphics::image`] to
32//! access pixel data and dimensions.
33//!
34//! ## Looking up images
35//!
36//! Given an image ID (obtained from a placement via
37//! [`PlacementIteration::image_id`]), call [`Graphics::image`] to get an
38//! [`Image`] handle. From this handle, various methods provide the
39//! [image dimensions](Image::width), [pixel format](Image::format),
40//! [compression](Image::compression), and a reference to the
41//! [raw pixel data](Image::data).
42//!
43//! ## Rendering helpers
44//!
45//! Several functions assist with rendering a placement:
46//!
47//! - [`PlacementIteration::pixel_size`] — rendered pixel
48//! dimensions accounting for source rect and aspect ratio.
49//! - [`PlacementIteration::grid_size`] — number of grid
50//! columns and rows the placement occupies.
51//! - [`PlacementIteration::viewport_pos`] — viewport-relative
52//! grid position (may be negative for partially scrolled placements).
53//! - [`PlacementIteration::source_rect`] — resolved source
54//! rectangle in pixels, clamped to image bounds.
55//! - [`PlacementIteration::rect`] — bounding rectangle as a
56//! [`Selection`].
57//!
58//! ## Lifetimes and thread-safety
59//!
60//! All handles borrowed from the terminal ([`Graphics`],
61//! [`Image`]) are invalidated by any mutating terminal
62//! call. The placement iterator is independently owned and must be freed
63//! by the caller, but the data it yields is only valid while the
64//! underlying terminal is not mutated.
65//!
66//! ## Example
67//!
68//! The following example creates a terminal, sends a Kitty graphics
69//! image, then iterates placements and prints image metadata:
70//!
71//! ```
72//! use libghostty_vt::{
73//! Terminal,
74//! TerminalOptions,
75//! alloc::{Allocator, Bytes},
76//! kitty::graphics,
77//! };
78//!
79//! /// Minimal PNG decoder.
80//! ///
81//! /// A real implementation would use a PNG library (libpng, stb_image, etc.)
82//! /// to decode the PNG data. This example uses a hardcoded 1x1 red pixel
83//! /// since we know exactly what image we're sending.
84//! ///
85//! /// WARNING: This is only an example for providing a callback, it DOES NOT
86//! /// actually decode the PNG it is passed. It hardcodes a response.
87//! struct StubPngDecoder;
88//!
89//! impl graphics::DecodePng for StubPngDecoder {
90//! fn decode_png<'alloc, 'ctx>(
91//! &mut self,
92//! alloc: &'alloc Allocator<'ctx>,
93//! data: &[u8],
94//! ) -> Option<graphics::DecodedImage<'alloc>> {
95//! // Allocate RGBA pixel data through the provided allocator.
96//! let mut data = Bytes::new_with_alloc(alloc, 4).ok()?;
97//!
98//! // Fill with red (R=255, G=0, B=0, A=255).
99//! data.copy_from_slice(&[255, 0, 0, 255]);
100//!
101//! Some(graphics::DecodedImage {
102//! width: 1,
103//! height: 1,
104//! data,
105//! })
106//! }
107//! }
108//!
109//! fn main() -> Result<(), Box<dyn std::error::Error>> {
110//! graphics::set_png_decoder(Some(Box::new(StubPngDecoder)))?;
111//!
112//! let mut terminal = Terminal::new(TerminalOptions {
113//! cols: 80,
114//! rows: 24,
115//! max_scrollback: 0
116//! })?;
117//!
118//! // Set cell pixel dimensions so kitty graphics can compute grid sizes.
119//! terminal.resize(80, 24, 8, 16)?;
120//!
121//! // Set a storage limit (64MiB) to enable Kitty graphics.
122//! terminal.set_kitty_image_storage_limit(64 * 1024 * 1024)?;
123//!
124//! // Install pty_write to see the protocol response.
125//! terminal.on_pty_write(|_, data| println!("{}", data.escape_ascii()))?;
126//!
127//! // Send a Kitty graphics command with an inline 1x1 PNG image.
128//! //
129//! // The escape sequence is:
130//! // ESC _G a=T,f=100,q=1; <base64 PNG data> ESC \
131//! //
132//! // Where:
133//! // a=T — transmit and display
134//! // f=100 — PNG format
135//! // q=1 — request a response (q=0 would suppress it)
136//! println!("Sending Kitty graphics PNG image:");
137//! terminal.vt_write(
138//! b"\x1b_Ga=T,f=100,q=1;\
139//! iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA\
140//! DUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==\
141//! \x1b\\"
142//! );
143//!
144//! let graphics = terminal.kitty_graphics()?;
145//! let mut iter = graphics::PlacementIterator::new()?;
146//! let mut placements = iter.update(&graphics)?;
147//!
148//! let mut placement_count = 0usize;
149//! while let Some(placement) = placements.next() {
150//! placement_count += 1;
151//! let image_id = placement.image_id()?;
152//! println!(
153//! " placement #{}: image_id={} placement_id={} virtual={} z={}",
154//! placement_count,
155//! image_id,
156//! placement.placement_id()?,
157//! placement.is_virtual()?,
158//! placement.z()?,
159//! );
160//!
161//! // Look up the image and print its properties.
162//! let image = graphics.image(image_id).unwrap();
163//! println!(
164//! " image: number={} size={}x{} format={:?} data_len={}",
165//! image.number()?,
166//! image.width()?,
167//! image.height()?,
168//! image.format()?,
169//! image.data()?.len(),
170//! );
171//!
172//! let pixel_size = placement.pixel_size(&image, &terminal)?;
173//! println!(
174//! " rendered pixel size: {}x{}",
175//! pixel_size.width, pixel_size.height,
176//! );
177//! let grid_size = placement.grid_size(&image, &terminal)?;
178//! println!(
179//! " grid size: {} cols x {} rows",
180//! grid_size.cols, grid_size.rows,
181//! );
182//! }
183//! println!("Total placements: {placement_count}");
184//! Ok(())
185//! }
186//! ```
187//!
188
189#![cfg(feature = "kitty-graphics")]
190
191use std::{
192 cell::RefCell,
193 mem::{ManuallyDrop, MaybeUninit},
194};
195
196use crate::{
197 Terminal,
198 alloc::{Allocator, Bytes, Object, Ref},
199 error::{Error, Result, from_optional_result_uninit, from_result},
200 ffi,
201 selection::Selection,
202};
203
204#[doc(inline)]
205pub use ffi::KittyGraphicsPlacementRenderInfo as PlacementRenderInfo;
206
207/// Opaque reference to a Kitty graphics image storage.
208///
209/// Obtained via [`Terminal::kitty_graphics`]. The reference is borrowed from
210/// the terminal with lifetime `'t` and remains valid until the next mutating
211/// terminal call (e.g. [`Terminal::vt_write`] or [`Terminal::reset`]).
212#[derive(Debug)]
213pub struct Graphics<'t> {
214 inner: Ref<'t, ffi::KittyGraphicsImpl>,
215}
216
217/// Opaque reference to a Kitty graphics image.
218///
219/// Obtained via [`Graphics::image`] with an image ID. The reference is
220/// borrowed from the storage with lifetime `'t` and remains valid until
221/// the next mutating terminal call.
222#[derive(Debug)]
223pub struct Image<'t> {
224 inner: Ref<'t, ffi::KittyGraphicsImageImpl>,
225}
226
227/// Opaque reference to a Kitty graphics placement iterator.
228#[derive(Debug)]
229pub struct PlacementIterator<'alloc> {
230 inner: Object<'alloc, ffi::KittyGraphicsPlacementIteratorImpl>,
231}
232
233/// Obtained via [`PlacementIterator::update`]. The reference is
234/// borrowed from the storage with lifetime `'t` and remains valid until
235/// the next mutating terminal call.
236#[derive(Debug)]
237pub struct PlacementIteration<'t, 'alloc>(&'t mut PlacementIterator<'alloc>);
238
239/// Methods related to the [Kitty graphics protocol](crate::kitty::graphics).
240impl Terminal<'_, '_> {
241 /// The Kitty graphics image storage for the active screen.
242 ///
243 /// Returns a borrowed reference to the image storage.
244 /// The pointer is valid until the next mutating terminal call (e.g.
245 /// [`Terminal::vt_write`] or [`Terminal::reset`]).
246 pub fn kitty_graphics(&self) -> Result<Graphics<'_>> {
247 let inner = self.get::<ffi::KittyGraphics>(ffi::TerminalData::KITTY_GRAPHICS)?;
248 Ok(Graphics {
249 inner: Ref::new(inner)?,
250 })
251 }
252
253 /// The Kitty image storage limit in bytes for the active screen.
254 ///
255 /// A value of zero means the Kitty graphics protocol is disabled.
256 pub fn kitty_image_storage_limit(&self) -> Result<u64> {
257 self.get(ffi::TerminalData::KITTY_IMAGE_STORAGE_LIMIT)
258 }
259 /// Whether the file medium is enabled for Kitty image loading on the
260 /// active screen.
261 pub fn is_kitty_image_from_file_allowed(&self) -> Result<bool> {
262 self.get(ffi::TerminalData::KITTY_IMAGE_MEDIUM_FILE)
263 }
264 /// Whether the temporary file medium is enabled for Kitty image loading
265 /// on the active screen.
266 pub fn is_kitty_image_from_temp_file_allowed(&self) -> Result<bool> {
267 self.get(ffi::TerminalData::KITTY_IMAGE_MEDIUM_TEMP_FILE)
268 }
269 /// Whether the shared memory medium is enabled for Kitty image loading
270 /// on the active screen.
271 pub fn is_kitty_image_from_shared_mem_allowed(&self) -> Result<bool> {
272 self.get(ffi::TerminalData::KITTY_IMAGE_MEDIUM_SHARED_MEM)
273 }
274 /// Set the Kitty image storage limit in bytes.
275 ///
276 /// Applied to all initialized screens (primary and alternate).
277 /// A value of zero disables the Kitty graphics protocol entirely,
278 /// deleting all stored images and placements.
279 pub fn set_kitty_image_storage_limit(&mut self, limit: u64) -> Result<&mut Self> {
280 self.set(ffi::TerminalOption::KITTY_IMAGE_STORAGE_LIMIT, &limit)?;
281 Ok(self)
282 }
283 /// Enable or disable Kitty image loading via the file medium.
284 ///
285 /// Has no effect when Kitty graphics are disabled at build time.
286 pub fn set_kitty_image_from_file_allowed(&mut self, allowed: bool) -> Result<&mut Self> {
287 self.set(ffi::TerminalOption::KITTY_IMAGE_MEDIUM_FILE, &allowed)?;
288 Ok(self)
289 }
290 /// Enable or disable Kitty image loading via the temporary file medium.
291 ///
292 /// Has no effect when Kitty graphics are disabled at build time.
293 pub fn set_kitty_image_from_temp_file_allowed(&mut self, allowed: bool) -> Result<&mut Self> {
294 self.set(ffi::TerminalOption::KITTY_IMAGE_MEDIUM_TEMP_FILE, &allowed)?;
295 Ok(self)
296 }
297 /// Enable or disable Kitty image loading via the shared memory medium.
298 ///
299 /// Has no effect when Kitty graphics are disabled at build time.
300 pub fn set_kitty_image_from_shared_mem_allowed(&mut self, allowed: bool) -> Result<&mut Self> {
301 self.set(ffi::TerminalOption::KITTY_IMAGE_MEDIUM_SHARED_MEM, &allowed)?;
302 Ok(self)
303 }
304
305 /// Set the maximum bytes the APC handler will buffer for Kitty graphics
306 /// protocol data.
307 ///
308 /// This prevents malicious input from causing unbounded memory allocation.
309 /// A `None` value removes all overrides, reverting to the built-in defaults.
310 pub fn set_apc_max_bytes_kitty(&mut self, max: Option<usize>) -> Result<&mut Self> {
311 self.set_optional(ffi::TerminalOption::APC_MAX_BYTES_KITTY, max.as_ref())?;
312 Ok(self)
313 }
314}
315
316impl<'t> Graphics<'t> {
317 /// Look up a Kitty graphics image by its image ID.
318 ///
319 /// Returns `None` if no image with the given ID exists.
320 pub fn image(&self, id: u32) -> Option<Image<'t>> {
321 let image = unsafe { ffi::ghostty_kitty_graphics_image(self.inner.as_raw(), id) };
322
323 Some(Image {
324 inner: Ref::new(image.cast_mut()).ok()?,
325 })
326 }
327}
328
329impl<'t> Image<'t> {
330 fn get<T>(&self, tag: ffi::KittyGraphicsImageData::Type) -> Result<T> {
331 let mut value = MaybeUninit::<T>::zeroed();
332 let result = unsafe {
333 ffi::ghostty_kitty_graphics_image_get(
334 self.inner.as_raw(),
335 tag,
336 value.as_mut_ptr().cast(),
337 )
338 };
339 // Since we manually model every possible query, this should never fail.
340 from_result(result)?;
341 // SAFETY: Value should be initialized after successful call.
342 Ok(unsafe { value.assume_init() })
343 }
344
345 /// The image ID.
346 pub fn id(&self) -> Result<u32> {
347 self.get(ffi::KittyGraphicsImageData::ID)
348 }
349 /// The image number.
350 pub fn number(&self) -> Result<u32> {
351 self.get(ffi::KittyGraphicsImageData::NUMBER)
352 }
353 /// Image width in pixels.
354 pub fn width(&self) -> Result<u32> {
355 self.get(ffi::KittyGraphicsImageData::WIDTH)
356 }
357 /// Image height in pixels.
358 pub fn height(&self) -> Result<u32> {
359 self.get(ffi::KittyGraphicsImageData::HEIGHT)
360 }
361 /// Pixel format of the image.
362 pub fn format(&self) -> Result<ImageFormat> {
363 self.get::<ffi::KittyImageFormat::Type>(ffi::KittyGraphicsImageData::FORMAT)
364 .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
365 }
366 /// Compression of the image.
367 pub fn compression(&self) -> Result<Compression> {
368 self.get::<ffi::KittyImageCompression::Type>(ffi::KittyGraphicsImageData::COMPRESSION)
369 .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
370 }
371 /// Borrowed pointer to the raw pixel data.
372 ///
373 /// Valid as long as the underlying terminal is not mutated.
374 pub fn data(&self) -> Result<&'t [u8]> {
375 let ptr = self.get::<*const u8>(ffi::KittyGraphicsImageData::DATA_PTR)?;
376 let len = self.get::<usize>(ffi::KittyGraphicsImageData::DATA_LEN)?;
377
378 // SAFETY: We trust libghostty to return valid results
379 Ok(unsafe { std::slice::from_raw_parts(ptr, len) })
380 }
381}
382
383impl<'alloc> PlacementIterator<'alloc> {
384 /// Create a new placement iterator instance.
385 pub fn new() -> Result<Self> {
386 // SAFETY: A NULL allocator is always valid
387 unsafe { Self::new_inner(std::ptr::null()) }
388 }
389 /// Create a new placement iterator instance with a custom allocator.
390 ///
391 /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
392 /// regarding custom memory management and lifetimes.
393 pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
394 // SAFETY: Borrow checking should forbid invalid allocators
395 unsafe { Self::new_inner(alloc.to_raw()) }
396 }
397 unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
398 let mut inner: ffi::KittyGraphicsPlacementIterator = std::ptr::null_mut();
399 let result =
400 unsafe { ffi::ghostty_kitty_graphics_placement_iterator_new(alloc, &raw mut inner) };
401 from_result(result)?;
402 Ok(Self {
403 inner: Object::new(inner)?,
404 })
405 }
406
407 /// Update the placement iterator with the given graphics storage,
408 /// returning a new placement iteration.
409 pub fn update(&mut self, graphics: &Graphics<'_>) -> Result<PlacementIteration<'_, 'alloc>> {
410 let result = unsafe {
411 ffi::ghostty_kitty_graphics_get(
412 graphics.inner.as_raw(),
413 ffi::KittyGraphicsData::PLACEMENT_ITERATOR,
414 (&raw mut self.inner).cast(),
415 )
416 };
417 from_result(result)?;
418 Ok(PlacementIteration(self))
419 }
420}
421
422impl Drop for PlacementIterator<'_> {
423 fn drop(&mut self) {
424 unsafe {
425 ffi::ghostty_kitty_graphics_placement_iterator_free(self.inner.as_raw());
426 }
427 }
428}
429
430impl<'t, 'alloc> PlacementIteration<'t, 'alloc> {
431 /// Advance the placement iterator to the next placement.
432 ///
433 /// If a layer filter has been set via [`PlacementIteration::set_layer`],
434 /// only placements matching that layer are returned.
435 pub fn next(&mut self) -> Option<&Self> {
436 if unsafe { ffi::ghostty_kitty_graphics_placement_next(self.0.inner.as_raw()) } {
437 Some(self)
438 } else {
439 None
440 }
441 }
442
443 fn set<T>(
444 &self,
445 tag: ffi::KittyGraphicsPlacementIteratorOption::Type,
446 value: &T,
447 ) -> Result<()> {
448 let result = unsafe {
449 ffi::ghostty_kitty_graphics_placement_iterator_set(
450 self.0.inner.as_raw(),
451 tag,
452 std::ptr::from_ref(value).cast(),
453 )
454 };
455 from_result(result)
456 }
457 fn get<T>(&self, tag: ffi::KittyGraphicsPlacementData::Type) -> Result<T> {
458 let mut value = MaybeUninit::<T>::zeroed();
459 let result = unsafe {
460 ffi::ghostty_kitty_graphics_placement_get(
461 self.0.inner.as_raw(),
462 tag,
463 value.as_mut_ptr().cast(),
464 )
465 };
466 // Since we manually model every possible query, this should never fail.
467 from_result(result)?;
468 // SAFETY: Value should be initialized after successful call.
469 Ok(unsafe { value.assume_init() })
470 }
471
472 /// Set the z-layer filter for the iterator.
473 pub fn set_layer(&self, layer: Layer) -> Result<()> {
474 self.set::<ffi::KittyPlacementLayer::Type>(
475 ffi::KittyGraphicsPlacementIteratorOption::LAYER,
476 &layer.into(),
477 )
478 }
479
480 /// Compute the rendered pixel size of the current placement.
481 ///
482 /// Takes into account the placement's source rectangle, specified
483 /// columns/rows, and aspect ratio to calculate the final rendered pixel
484 /// dimensions.
485 pub fn pixel_size(
486 &self,
487 image: &Image<'t>,
488 terminal: &'t Terminal<'_, '_>,
489 ) -> Result<PixelSize> {
490 let mut size = PixelSize::default();
491 let result = unsafe {
492 ffi::ghostty_kitty_graphics_placement_pixel_size(
493 self.0.inner.as_raw(),
494 image.inner.as_raw(),
495 terminal.inner.as_raw(),
496 &raw mut size.width,
497 &raw mut size.height,
498 )
499 };
500 from_result(result)?;
501 Ok(size)
502 }
503
504 /// Compute the rendered pixel size of the current placement.
505 ///
506 /// Takes into account the placement's source rectangle, specified
507 /// columns/rows, and aspect ratio to calculate the final rendered pixel
508 /// dimensions.
509 pub fn grid_size(&self, image: &Image<'t>, terminal: &'t Terminal<'_, '_>) -> Result<GridSize> {
510 let mut size = GridSize::default();
511 let result = unsafe {
512 ffi::ghostty_kitty_graphics_placement_grid_size(
513 self.0.inner.as_raw(),
514 image.inner.as_raw(),
515 terminal.inner.as_raw(),
516 &raw mut size.cols,
517 &raw mut size.rows,
518 )
519 };
520 from_result(result)?;
521 Ok(size)
522 }
523
524 /// Get the viewport-relative grid position of the current placement.
525 ///
526 /// Converts the placement's internal pin to viewport-relative column and
527 /// row coordinates. The returned coordinates represent the top-left
528 /// corner of the placement in the viewport's grid coordinate space.
529 ///
530 /// The row value can be negative when the placement's origin has
531 /// scrolled above the top of the viewport. For example, a 4-row
532 /// image that has scrolled up by 2 rows returns row=-2, meaning
533 /// its top 2 rows are above the visible area but its bottom 2 rows
534 /// are still on screen. Embedders should use these coordinates
535 /// directly when computing the destination rectangle for rendering;
536 /// the embedder is responsible for clipping the portion of the image
537 /// that falls outside the viewport.
538 ///
539 /// Returns `None` when the placement is completely outside the viewport
540 /// (its bottom edge is above the viewport or its top edge is at or below
541 /// the last viewport row), or when the placement is a virtual (unicode
542 /// placeholder) placement.
543 pub fn viewport_pos(
544 &self,
545 image: &Image<'t>,
546 terminal: &'t Terminal<'_, '_>,
547 ) -> Result<Option<ViewportPos>> {
548 let mut pos = ViewportPos::default();
549 let result = unsafe {
550 ffi::ghostty_kitty_graphics_placement_viewport_pos(
551 self.0.inner.as_raw(),
552 image.inner.as_raw(),
553 terminal.inner.as_raw(),
554 &raw mut pos.col,
555 &raw mut pos.row,
556 )
557 };
558 from_optional_result_uninit(result, MaybeUninit::new(pos))
559 }
560
561 /// Get the resolved source rectangle for the current placement.
562 ///
563 /// Applies kitty protocol semantics: a width or height of 0 in the
564 /// placement means "use the full image dimension", and the resulting
565 /// rectangle is clamped to the actual image bounds. The returned values
566 /// are in pixels and are ready to use for texture sampling.
567 pub fn source_rect(&self, image: &Image<'t>) -> Result<SourceRect> {
568 let mut rect = SourceRect::default();
569 let result = unsafe {
570 ffi::ghostty_kitty_graphics_placement_source_rect(
571 self.0.inner.as_raw(),
572 image.inner.as_raw(),
573 &raw mut rect.x,
574 &raw mut rect.y,
575 &raw mut rect.width,
576 &raw mut rect.height,
577 )
578 };
579 from_result(result)?;
580 Ok(rect)
581 }
582
583 /// Get the resolved source rectangle for the current placement.
584 ///
585 /// Applies kitty protocol semantics: a width or height of 0 in the
586 /// placement means "use the full image dimension", and the resulting
587 /// rectangle is clamped to the actual image bounds. The returned values
588 /// are in pixels and are ready to use for texture sampling.
589 pub fn rect(&self, image: &Image<'t>, terminal: &'t Terminal<'_, '_>) -> Result<Selection<'t>> {
590 let mut sel = MaybeUninit::<ffi::Selection>::zeroed();
591 let result = unsafe {
592 ffi::ghostty_kitty_graphics_placement_rect(
593 self.0.inner.as_raw(),
594 image.inner.as_raw(),
595 terminal.inner.as_raw(),
596 sel.as_mut_ptr(),
597 )
598 };
599 from_result(result)?;
600 // SAFETY: Selection should be initialized and valid on success
601 Ok(unsafe { Selection::from_raw(sel.assume_init()) })
602 }
603
604 /// Get all rendering geometry for a placement in a single call.
605 ///
606 /// Combines pixel size, grid size, viewport position, and source
607 /// rectangle into one struct.
608 ///
609 /// When `viewport_visible` is false, the placement is fully off-screen
610 /// or is a virtual placement; `viewport_col` and `viewport_row` may
611 /// contain meaningless values in that case.
612 pub fn placement_render_info(
613 &self,
614 image: &Image<'t>,
615 terminal: &'t Terminal<'_, '_>,
616 ) -> Result<PlacementRenderInfo> {
617 let mut info = ffi::sized!(PlacementRenderInfo);
618 let result = unsafe {
619 ffi::ghostty_kitty_graphics_placement_render_info(
620 self.0.inner.as_raw(),
621 image.inner.as_raw(),
622 terminal.inner.as_raw(),
623 &raw mut info,
624 )
625 };
626 from_result(result)?;
627 Ok(info)
628 }
629
630 /// The image ID this placement belongs to.
631 pub fn image_id(&self) -> Result<u32> {
632 self.get(ffi::KittyGraphicsPlacementData::IMAGE_ID)
633 }
634 /// The image ID this placement belongs to.
635 pub fn placement_id(&self) -> Result<u32> {
636 self.get(ffi::KittyGraphicsPlacementData::PLACEMENT_ID)
637 }
638 /// Whether this is a virtual placement (unicode placeholder).
639 pub fn is_virtual(&self) -> Result<bool> {
640 self.get(ffi::KittyGraphicsPlacementData::IS_VIRTUAL)
641 }
642 /// Pixel offset from the left edge of the cell.
643 pub fn x_offset(&self) -> Result<u32> {
644 self.get(ffi::KittyGraphicsPlacementData::X_OFFSET)
645 }
646 /// Pixel offset from the top edge of the cell.
647 pub fn y_offset(&self) -> Result<u32> {
648 self.get(ffi::KittyGraphicsPlacementData::Y_OFFSET)
649 }
650 /// Source rectangle x origin in pixels.
651 pub fn source_x(&self) -> Result<u32> {
652 self.get(ffi::KittyGraphicsPlacementData::SOURCE_X)
653 }
654 /// Source rectangle y origin in pixels.
655 pub fn source_y(&self) -> Result<u32> {
656 self.get(ffi::KittyGraphicsPlacementData::SOURCE_Y)
657 }
658 /// Source rectangle width in pixels (0 = full image width).
659 pub fn source_width(&self) -> Result<u32> {
660 self.get(ffi::KittyGraphicsPlacementData::SOURCE_WIDTH)
661 }
662 /// Source rectangle height in pixels (0 = full image height).
663 pub fn source_height(&self) -> Result<u32> {
664 self.get(ffi::KittyGraphicsPlacementData::SOURCE_HEIGHT)
665 }
666 /// Number of columns this placement occupies.
667 pub fn columns(&self) -> Result<u32> {
668 self.get(ffi::KittyGraphicsPlacementData::COLUMNS)
669 }
670 /// Number of rows this placement occupies.
671 pub fn rows(&self) -> Result<u32> {
672 self.get(ffi::KittyGraphicsPlacementData::ROWS)
673 }
674 /// Z-index for this placement.
675 pub fn z(&self) -> Result<i32> {
676 self.get(ffi::KittyGraphicsPlacementData::Z)
677 }
678}
679
680/// The size of an image in pixel coordinates.
681#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
682pub struct PixelSize {
683 /// The width in number of pixels.
684 pub width: u32,
685 /// The height in number of pixels.
686 pub height: u32,
687}
688
689/// The size of an image in grid coordinates.
690#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
691pub struct GridSize {
692 /// The number of columns.
693 pub cols: u32,
694 /// The number of rows.
695 pub rows: u32,
696}
697
698/// The position of an image in the viewport.
699///
700/// The row value can be negative when the placement's origin has
701/// scrolled above the top of the viewport. For example, a 4-row
702/// image that has scrolled up by 2 rows returns row=-2, meaning
703/// its top 2 rows are above the visible area but its bottom 2 rows
704/// are still on screen. Embedders should use these coordinates
705/// directly when computing the destination rectangle for rendering;
706/// the embedder is responsible for clipping the portion of the image
707/// that falls outside the viewport.
708#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
709pub struct ViewportPos {
710 /// The column index relative to the viewport.
711 pub col: i32,
712 /// The row index relative to the viewport.
713 pub row: i32,
714}
715
716/// The pixel position and size of a source rectangle.
717#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
718pub struct SourceRect {
719 /// The x origin in pixels.
720 pub x: u32,
721 /// The y origin in pixels.
722 pub y: u32,
723 /// The width in pixels.
724 pub width: u32,
725 /// The height in pixels.
726 pub height: u32,
727}
728
729/// Z-layer classification for kitty graphics placements.
730#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, int_enum::IntEnum)]
731#[repr(u32)]
732pub enum Layer {
733 /// Match all placements; apply no filtering (default behavior).
734 #[default]
735 All = ffi::KittyPlacementLayer::ALL,
736 /// Match placements positioned below the cell background (z < [`i32::MIN`] / 2).
737 BelowBg = ffi::KittyPlacementLayer::BELOW_BG,
738 /// Match placements positioned above the cell background and below text
739 /// ([`i32::MIN`] / 2 ≤ z < 0).
740 BelowText = ffi::KittyPlacementLayer::BELOW_TEXT,
741 /// Match placements positioned above text (z ≥ 0).
742 AboveText = ffi::KittyPlacementLayer::ABOVE_TEXT,
743}
744
745/// Pixel format of a Kitty graphics image.
746#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, int_enum::IntEnum)]
747#[non_exhaustive]
748#[repr(u32)]
749#[expect(missing_docs, reason = "missing upstream docs")]
750pub enum ImageFormat {
751 #[default]
752 Rgb = ffi::KittyImageFormat::RGB,
753 Rgba = ffi::KittyImageFormat::RGBA,
754 Png = ffi::KittyImageFormat::PNG,
755 GrayAlpha = ffi::KittyImageFormat::GRAY_ALPHA,
756 Gray = ffi::KittyImageFormat::GRAY,
757}
758
759/// Compression of a Kitty graphics image.
760#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, int_enum::IntEnum)]
761#[non_exhaustive]
762#[repr(u32)]
763#[expect(missing_docs, reason = "missing upstream docs")]
764pub enum Compression {
765 #[default]
766 None = ffi::KittyImageCompression::NONE,
767 ZlibDeflate = ffi::KittyImageCompression::ZLIB_DEFLATE,
768}
769
770// Unlike other sys functions (e.g. `log::set_logger`), the decoder
771// callback will only ever be called on
772thread_local! {
773 static DECODE_PNG: RefCell<Option<Box<dyn DecodePng>>> = RefCell::new(None);
774}
775
776/// Set the PNG decoder.
777///
778/// When set, the terminal can accept PNG images via the Kitty Graphics Protocol.
779/// When cleared (`None` value), PNG decoding is unsupported and PNG image data
780/// will be rejected.
781///
782/// # Thread safety
783///
784/// This function must only be called on the same thread as the terminal
785pub fn set_png_decoder(f: Option<Box<dyn DecodePng>>) -> Result<()> {
786 unsafe extern "C" fn callback(
787 _userdata: *mut std::ffi::c_void,
788 allocator: *const ffi::Allocator,
789 data: *const u8,
790 data_len: usize,
791 out: *mut ffi::SysImage,
792 ) -> bool {
793 DECODE_PNG.with_borrow_mut(|decoder| {
794 let Some(decoder) = decoder else {
795 return false;
796 };
797 // SAFETY: We trust libghostty to return valid values.
798 let alloc = unsafe { Allocator::from_raw(allocator) };
799 let data = unsafe { std::slice::from_raw_parts(data, data_len) };
800
801 match decoder.decode_png(&alloc, data) {
802 Some(result) => {
803 // IMPORTANT: Do NOT run the Rust destructor here
804 // to avoid double-freeing the byte buffer.
805 let mut result = ManuallyDrop::new(result);
806 unsafe {
807 *out = ffi::SysImage {
808 width: result.width,
809 height: result.height,
810 data: result.data.as_mut_ptr(),
811 data_len: result.data.len(),
812 }
813 };
814 true
815 }
816 None => false,
817 }
818 })
819 }
820
821 // Write out the matches here to coerce function items into function
822 // pointers, and trait impls into boxed trait objects. Yes, this is
823 // the simplest way to do so.
824 let ptr: ffi::SysDecodePngFn = match f {
825 None => None,
826 Some(_) => Some(callback),
827 };
828 DECODE_PNG.replace(f);
829
830 crate::sys_set(
831 ffi::SysOption::GHOSTTY_SYS_OPT_DECODE_PNG,
832 ptr.map_or(std::ptr::null(), |p| p as *const std::ffi::c_void),
833 )
834}
835
836/// A PNG decoder that can be used by the Kitty graphics protocol
837/// to decode PNG images into 8-bit RGBA pixels.
838///
839/// See [`set_png_decoder`] for more details.
840pub trait DecodePng: 'static {
841 /// Decode a PNG into 8-bit RGBA pixels.
842 ///
843 /// The returned image's byte buffer *must* be allocated by
844 /// the provided allocator.
845 fn decode_png<'alloc>(
846 &mut self,
847 alloc: &'alloc Allocator<'_>,
848 data: &[u8],
849 ) -> Option<DecodedImage<'alloc>>;
850}
851
852/// A PNG decoder for [`set_png_decoder`] using the [`png`] crate.
853///
854/// ```rust
855/// use ghostty::kitty::graphics;
856///
857/// graphics::set_png_decoder(RustPngDecoder::new());
858/// ```
859#[cfg(all(feature = "kitty-graphics", feature = "png"))]
860#[derive(Clone, Debug)]
861pub struct RustPngDecoder {
862 buf: Vec<u8>,
863}
864#[cfg(all(feature = "kitty-graphics", feature = "png"))]
865impl DecodePng for RustPngDecoder {
866 fn decode_png<'alloc>(
867 &mut self,
868 alloc: &'alloc Allocator<'_>,
869 data: &[u8],
870 ) -> Option<DecodedImage<'alloc>> {
871 use png::{Decoder, Transformations};
872 use std::io::Cursor;
873
874 let mut decoder = Decoder::new(Cursor::new(data));
875
876 // libghostty only accepts RGBA8 data, so we have to apply some
877 // transformations to accept images in other formats, namely
878 // expanding palette and grayscale colors to RGBA8 and stripping
879 // 16-bit color depth information back down into 8-bit.
880 decoder.set_transformations(Transformations::ALPHA | Transformations::STRIP_16);
881
882 let mut frame = decoder.read_info().ok()?;
883 let buf_size = frame.output_buffer_size()?;
884 if buf_size > self.buf.capacity() {
885 self.buf.reserve(buf_size - self.buf.capacity());
886 }
887 self.buf.fill(0);
888
889 let info = frame.next_frame(&mut self.buf).ok()?;
890
891 let mut bytes = Bytes::new_with_alloc(alloc, info.buffer_size()).ok()?;
892 bytes.copy_from_slice(&self.buf[..info.buffer_size()]);
893 frame.finish().ok()?;
894
895 Some(DecodedImage {
896 width: info.width,
897 height: info.height,
898 data: bytes,
899 })
900 }
901}
902
903/// Result of decoding an image.
904///
905/// The `data` buffer must be allocated through the allocator provided to the
906/// decode callback. The library takes ownership and will free it with the
907/// same allocator.
908#[derive(Debug)]
909pub struct DecodedImage<'alloc> {
910 /// Image width in pixels.
911 pub width: u32,
912 /// Image height in pixels.
913 pub height: u32,
914 /// Byte buffer containing the decoded RGBA pixel data.
915 pub data: Bytes<'alloc>,
916}
917impl From<DecodedImage<'_>> for ffi::SysImage {
918 fn from(mut value: DecodedImage<'_>) -> Self {
919 Self {
920 width: value.width,
921 height: value.height,
922 data: value.data.as_mut_ptr(),
923 data_len: value.data.len(),
924 }
925 }
926}