Skip to main content

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    fn get<T>(&self, tag: ffi::KittyGraphicsData::Type) -> Result<T> {
318        let mut value = MaybeUninit::<T>::zeroed();
319        let result = unsafe {
320            ffi::ghostty_kitty_graphics_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
321        };
322        from_result(result)?;
323        Ok(unsafe { value.assume_init() })
324    }
325
326    /// Look up a Kitty graphics image by its image ID.
327    ///
328    /// Returns `None` if no image with the given ID exists.
329    pub fn image(&self, id: u32) -> Option<Image<'t>> {
330        let image = unsafe { ffi::ghostty_kitty_graphics_image(self.inner.as_raw(), id) };
331
332        Some(Image {
333            inner: Ref::new(image.cast_mut()).ok()?,
334        })
335    }
336
337    /// Generation stamp of the last content mutation to this storage.
338    ///
339    /// Content mutations are any image transmit or replace, placement add, or
340    /// delete. Zero means the storage has never been mutated and is therefore
341    /// empty.
342    ///
343    /// If the generation is unchanged since a previous query, the set of
344    /// placements and all image data are identical, so placement iteration and
345    /// image staleness checks can be skipped entirely. Note that placement
346    /// geometry may still have changed, since scrolling and resizing move
347    /// placements without changing the storage contents, so rendering geometry
348    /// must still be recomputed for frames marked dirty.
349    ///
350    /// Stamps are unique and monotonically increasing process-wide: a value
351    /// observed from any storage never recurs for different content, even across
352    /// screen switches or terminal resets. It is therefore safe to key caches on
353    /// this value alone.
354    pub fn generation(&self) -> Result<u64> {
355        self.get(ffi::KittyGraphicsData::GENERATION)
356    }
357}
358
359impl<'t> Image<'t> {
360    fn get<T>(&self, tag: ffi::KittyGraphicsImageData::Type) -> Result<T> {
361        let mut value = MaybeUninit::<T>::zeroed();
362        let result = unsafe {
363            ffi::ghostty_kitty_graphics_image_get(
364                self.inner.as_raw(),
365                tag,
366                value.as_mut_ptr().cast(),
367            )
368        };
369        // Since we manually model every possible query, this should never fail.
370        from_result(result)?;
371        // SAFETY: Value should be initialized after successful call.
372        Ok(unsafe { value.assume_init() })
373    }
374
375    /// The image ID.
376    pub fn id(&self) -> Result<u32> {
377        self.get(ffi::KittyGraphicsImageData::ID)
378    }
379    /// The image number.
380    pub fn number(&self) -> Result<u32> {
381        self.get(ffi::KittyGraphicsImageData::NUMBER)
382    }
383    /// Image width in pixels.
384    pub fn width(&self) -> Result<u32> {
385        self.get(ffi::KittyGraphicsImageData::WIDTH)
386    }
387    /// Image height in pixels.
388    pub fn height(&self) -> Result<u32> {
389        self.get(ffi::KittyGraphicsImageData::HEIGHT)
390    }
391    /// Generation stamp assigned when this image was added to or replaced in the storage.
392    ///
393    /// A changed generation for a given image ID means the pixel contents may
394    /// have changed even when the dimensions, format, and data length are
395    /// identical, for example a retransmission of the same image ID, so texture
396    /// caches must key staleness on this value rather than on size heuristics.
397    ///
398    /// Stamps are unique and monotonically increasing process-wide and are drawn
399    /// from the same sequence as [`Graphics::generation`]. Never zero for a
400    /// stored image, so zero can be used as an empty sentinel by callers.
401    pub fn generation(&self) -> Result<u64> {
402        self.get(ffi::KittyGraphicsImageData::GENERATION)
403    }
404    /// Pixel format of the image.
405    pub fn format(&self) -> Result<ImageFormat> {
406        self.get::<ffi::KittyImageFormat::Type>(ffi::KittyGraphicsImageData::FORMAT)
407            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
408    }
409    /// Compression of the image.
410    pub fn compression(&self) -> Result<Compression> {
411        self.get::<ffi::KittyImageCompression::Type>(ffi::KittyGraphicsImageData::COMPRESSION)
412            .and_then(|v| v.try_into().map_err(|_| Error::InvalidValue))
413    }
414    /// Borrowed pointer to the raw pixel data.
415    ///
416    /// Valid as long as the underlying terminal is not mutated.
417    pub fn data(&self) -> Result<&'t [u8]> {
418        let ptr = self.get::<*const u8>(ffi::KittyGraphicsImageData::DATA_PTR)?;
419        let len = self.get::<usize>(ffi::KittyGraphicsImageData::DATA_LEN)?;
420
421        // SAFETY: We trust libghostty to return valid results
422        Ok(unsafe { std::slice::from_raw_parts(ptr, len) })
423    }
424}
425
426impl<'alloc> PlacementIterator<'alloc> {
427    /// Create a new placement iterator instance.
428    pub fn new() -> Result<Self> {
429        // SAFETY: A NULL allocator is always valid
430        unsafe { Self::new_inner(std::ptr::null()) }
431    }
432    /// Create a new placement iterator instance with a custom allocator.
433    ///
434    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
435    /// regarding custom memory management and lifetimes.
436    pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
437        // SAFETY: Borrow checking should forbid invalid allocators
438        unsafe { Self::new_inner(alloc.to_raw()) }
439    }
440    unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
441        let mut inner: ffi::KittyGraphicsPlacementIterator = std::ptr::null_mut();
442        let result =
443            unsafe { ffi::ghostty_kitty_graphics_placement_iterator_new(alloc, &raw mut inner) };
444        from_result(result)?;
445        Ok(Self {
446            inner: Object::new(inner)?,
447        })
448    }
449
450    /// Update the placement iterator with the given graphics storage,
451    /// returning a new placement iteration.
452    pub fn update(&mut self, graphics: &Graphics<'_>) -> Result<PlacementIteration<'_, 'alloc>> {
453        let result = unsafe {
454            ffi::ghostty_kitty_graphics_get(
455                graphics.inner.as_raw(),
456                ffi::KittyGraphicsData::PLACEMENT_ITERATOR,
457                (&raw mut self.inner).cast(),
458            )
459        };
460        from_result(result)?;
461        Ok(PlacementIteration(self))
462    }
463}
464
465impl Drop for PlacementIterator<'_> {
466    fn drop(&mut self) {
467        unsafe {
468            ffi::ghostty_kitty_graphics_placement_iterator_free(self.inner.as_raw());
469        }
470    }
471}
472
473impl<'t, 'alloc> PlacementIteration<'t, 'alloc> {
474    /// Advance the placement iterator to the next placement.
475    ///
476    /// If a layer filter has been set via [`PlacementIteration::set_layer`],
477    /// only placements matching that layer are returned.
478    pub fn next(&mut self) -> Option<&Self> {
479        if unsafe { ffi::ghostty_kitty_graphics_placement_next(self.0.inner.as_raw()) } {
480            Some(self)
481        } else {
482            None
483        }
484    }
485
486    fn set<T>(
487        &self,
488        tag: ffi::KittyGraphicsPlacementIteratorOption::Type,
489        value: &T,
490    ) -> Result<()> {
491        let result = unsafe {
492            ffi::ghostty_kitty_graphics_placement_iterator_set(
493                self.0.inner.as_raw(),
494                tag,
495                std::ptr::from_ref(value).cast(),
496            )
497        };
498        from_result(result)
499    }
500    fn get<T>(&self, tag: ffi::KittyGraphicsPlacementData::Type) -> Result<T> {
501        let mut value = MaybeUninit::<T>::zeroed();
502        let result = unsafe {
503            ffi::ghostty_kitty_graphics_placement_get(
504                self.0.inner.as_raw(),
505                tag,
506                value.as_mut_ptr().cast(),
507            )
508        };
509        // Since we manually model every possible query, this should never fail.
510        from_result(result)?;
511        // SAFETY: Value should be initialized after successful call.
512        Ok(unsafe { value.assume_init() })
513    }
514
515    /// Set the z-layer filter for the iterator.
516    pub fn set_layer(&self, layer: Layer) -> Result<()> {
517        self.set::<ffi::KittyPlacementLayer::Type>(
518            ffi::KittyGraphicsPlacementIteratorOption::LAYER,
519            &layer.into(),
520        )
521    }
522
523    /// Compute the rendered pixel size of the current placement.
524    ///
525    /// Takes into account the placement's source rectangle, specified
526    /// columns/rows, and aspect ratio to calculate the final rendered pixel
527    /// dimensions.
528    pub fn pixel_size(
529        &self,
530        image: &Image<'t>,
531        terminal: &'t Terminal<'_, '_>,
532    ) -> Result<PixelSize> {
533        let mut size = PixelSize::default();
534        let result = unsafe {
535            ffi::ghostty_kitty_graphics_placement_pixel_size(
536                self.0.inner.as_raw(),
537                image.inner.as_raw(),
538                terminal.inner.as_raw(),
539                &raw mut size.width,
540                &raw mut size.height,
541            )
542        };
543        from_result(result)?;
544        Ok(size)
545    }
546
547    /// Compute the rendered pixel size of the current placement.
548    ///
549    /// Takes into account the placement's source rectangle, specified
550    /// columns/rows, and aspect ratio to calculate the final rendered pixel
551    /// dimensions.
552    pub fn grid_size(&self, image: &Image<'t>, terminal: &'t Terminal<'_, '_>) -> Result<GridSize> {
553        let mut size = GridSize::default();
554        let result = unsafe {
555            ffi::ghostty_kitty_graphics_placement_grid_size(
556                self.0.inner.as_raw(),
557                image.inner.as_raw(),
558                terminal.inner.as_raw(),
559                &raw mut size.cols,
560                &raw mut size.rows,
561            )
562        };
563        from_result(result)?;
564        Ok(size)
565    }
566
567    /// Get the viewport-relative grid position of the current placement.
568    ///
569    /// Converts the placement's internal pin to viewport-relative column and
570    /// row coordinates. The returned coordinates represent the top-left
571    /// corner of the placement in the viewport's grid coordinate space.
572    ///
573    /// The row value can be negative when the placement's origin has
574    /// scrolled above the top of the viewport. For example, a 4-row
575    /// image that has scrolled up by 2 rows returns row=-2, meaning
576    /// its top 2 rows are above the visible area but its bottom 2 rows
577    /// are still on screen. Embedders should use these coordinates
578    /// directly when computing the destination rectangle for rendering;
579    /// the embedder is responsible for clipping the portion of the image
580    /// that falls outside the viewport.
581    ///
582    /// Returns `None` when the placement is completely outside the viewport
583    /// (its bottom edge is above the viewport or its top edge is at or below
584    /// the last viewport row), or when the placement is a virtual (unicode
585    /// placeholder) placement.
586    pub fn viewport_pos(
587        &self,
588        image: &Image<'t>,
589        terminal: &'t Terminal<'_, '_>,
590    ) -> Result<Option<ViewportPos>> {
591        let mut pos = ViewportPos::default();
592        let result = unsafe {
593            ffi::ghostty_kitty_graphics_placement_viewport_pos(
594                self.0.inner.as_raw(),
595                image.inner.as_raw(),
596                terminal.inner.as_raw(),
597                &raw mut pos.col,
598                &raw mut pos.row,
599            )
600        };
601        from_optional_result_uninit(result, MaybeUninit::new(pos))
602    }
603
604    /// Get the resolved source rectangle for the current placement.
605    ///
606    /// Applies kitty protocol semantics: a width or height of 0 in the
607    /// placement means "use the full image dimension", and the resulting
608    /// rectangle is clamped to the actual image bounds. The returned values
609    /// are in pixels and are ready to use for texture sampling.
610    pub fn source_rect(&self, image: &Image<'t>) -> Result<SourceRect> {
611        let mut rect = SourceRect::default();
612        let result = unsafe {
613            ffi::ghostty_kitty_graphics_placement_source_rect(
614                self.0.inner.as_raw(),
615                image.inner.as_raw(),
616                &raw mut rect.x,
617                &raw mut rect.y,
618                &raw mut rect.width,
619                &raw mut rect.height,
620            )
621        };
622        from_result(result)?;
623        Ok(rect)
624    }
625
626    /// Get the resolved source rectangle for the current placement.
627    ///
628    /// Applies kitty protocol semantics: a width or height of 0 in the
629    /// placement means "use the full image dimension", and the resulting
630    /// rectangle is clamped to the actual image bounds. The returned values
631    /// are in pixels and are ready to use for texture sampling.
632    pub fn rect(&self, image: &Image<'t>, terminal: &'t Terminal<'_, '_>) -> Result<Selection<'t>> {
633        let mut sel = MaybeUninit::<ffi::Selection>::zeroed();
634        let result = unsafe {
635            ffi::ghostty_kitty_graphics_placement_rect(
636                self.0.inner.as_raw(),
637                image.inner.as_raw(),
638                terminal.inner.as_raw(),
639                sel.as_mut_ptr(),
640            )
641        };
642        from_result(result)?;
643        // SAFETY: Selection should be initialized and valid on success
644        Ok(unsafe { Selection::from_raw(sel.assume_init()) })
645    }
646
647    /// Get all rendering geometry for a placement in a single call.
648    ///
649    /// Combines pixel size, grid size, viewport position, and source
650    /// rectangle into one struct.
651    ///
652    /// When `viewport_visible` is false, the placement is fully off-screen
653    /// or is a virtual placement; `viewport_col` and `viewport_row` may
654    /// contain meaningless values in that case.
655    pub fn placement_render_info(
656        &self,
657        image: &Image<'t>,
658        terminal: &'t Terminal<'_, '_>,
659    ) -> Result<PlacementRenderInfo> {
660        let mut info = ffi::sized!(PlacementRenderInfo);
661        let result = unsafe {
662            ffi::ghostty_kitty_graphics_placement_render_info(
663                self.0.inner.as_raw(),
664                image.inner.as_raw(),
665                terminal.inner.as_raw(),
666                &raw mut info,
667            )
668        };
669        from_result(result)?;
670        Ok(info)
671    }
672
673    /// The image ID this placement belongs to.
674    pub fn image_id(&self) -> Result<u32> {
675        self.get(ffi::KittyGraphicsPlacementData::IMAGE_ID)
676    }
677    /// The image ID this placement belongs to.
678    pub fn placement_id(&self) -> Result<u32> {
679        self.get(ffi::KittyGraphicsPlacementData::PLACEMENT_ID)
680    }
681    /// Whether this is a virtual placement (unicode placeholder).
682    pub fn is_virtual(&self) -> Result<bool> {
683        self.get(ffi::KittyGraphicsPlacementData::IS_VIRTUAL)
684    }
685    /// Pixel offset from the left edge of the cell.
686    pub fn x_offset(&self) -> Result<u32> {
687        self.get(ffi::KittyGraphicsPlacementData::X_OFFSET)
688    }
689    /// Pixel offset from the top edge of the cell.
690    pub fn y_offset(&self) -> Result<u32> {
691        self.get(ffi::KittyGraphicsPlacementData::Y_OFFSET)
692    }
693    /// Source rectangle x origin in pixels.
694    pub fn source_x(&self) -> Result<u32> {
695        self.get(ffi::KittyGraphicsPlacementData::SOURCE_X)
696    }
697    /// Source rectangle y origin in pixels.
698    pub fn source_y(&self) -> Result<u32> {
699        self.get(ffi::KittyGraphicsPlacementData::SOURCE_Y)
700    }
701    /// Source rectangle width in pixels (0 = full image width).
702    pub fn source_width(&self) -> Result<u32> {
703        self.get(ffi::KittyGraphicsPlacementData::SOURCE_WIDTH)
704    }
705    /// Source rectangle height in pixels (0 = full image height).
706    pub fn source_height(&self) -> Result<u32> {
707        self.get(ffi::KittyGraphicsPlacementData::SOURCE_HEIGHT)
708    }
709    /// Number of columns this placement occupies.
710    pub fn columns(&self) -> Result<u32> {
711        self.get(ffi::KittyGraphicsPlacementData::COLUMNS)
712    }
713    /// Number of rows this placement occupies.
714    pub fn rows(&self) -> Result<u32> {
715        self.get(ffi::KittyGraphicsPlacementData::ROWS)
716    }
717    /// Z-index for this placement.
718    pub fn z(&self) -> Result<i32> {
719        self.get(ffi::KittyGraphicsPlacementData::Z)
720    }
721}
722
723/// The size of an image in pixel coordinates.
724#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
725pub struct PixelSize {
726    /// The width in number of pixels.
727    pub width: u32,
728    /// The height in number of pixels.
729    pub height: u32,
730}
731
732/// The size of an image in grid coordinates.
733#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
734pub struct GridSize {
735    /// The number of columns.
736    pub cols: u32,
737    /// The number of rows.
738    pub rows: u32,
739}
740
741/// The position of an image in the viewport.
742///
743/// The row value can be negative when the placement's origin has
744/// scrolled above the top of the viewport. For example, a 4-row
745/// image that has scrolled up by 2 rows returns row=-2, meaning
746/// its top 2 rows are above the visible area but its bottom 2 rows
747/// are still on screen. Embedders should use these coordinates
748/// directly when computing the destination rectangle for rendering;
749/// the embedder is responsible for clipping the portion of the image
750/// that falls outside the viewport.
751#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
752pub struct ViewportPos {
753    /// The column index relative to the viewport.
754    pub col: i32,
755    /// The row index relative to the viewport.
756    pub row: i32,
757}
758
759/// The pixel position and size of a source rectangle.
760#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
761pub struct SourceRect {
762    /// The x origin in pixels.
763    pub x: u32,
764    /// The y origin in pixels.
765    pub y: u32,
766    /// The width in pixels.
767    pub width: u32,
768    /// The height in pixels.
769    pub height: u32,
770}
771
772/// Z-layer classification for kitty graphics placements.
773#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, int_enum::IntEnum)]
774#[repr(u32)]
775pub enum Layer {
776    /// Match all placements; apply no filtering (default behavior).
777    #[default]
778    All = ffi::KittyPlacementLayer::ALL,
779    /// Match placements positioned below the cell background (z < [`i32::MIN`] / 2).
780    BelowBg = ffi::KittyPlacementLayer::BELOW_BG,
781    /// Match placements positioned above the cell background and below text
782    /// ([`i32::MIN`] / 2 ≤ z < 0).
783    BelowText = ffi::KittyPlacementLayer::BELOW_TEXT,
784    /// Match placements positioned above text (z ≥ 0).
785    AboveText = ffi::KittyPlacementLayer::ABOVE_TEXT,
786}
787
788/// Pixel format of a Kitty graphics image.
789#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, int_enum::IntEnum)]
790#[non_exhaustive]
791#[repr(u32)]
792#[expect(missing_docs, reason = "missing upstream docs")]
793pub enum ImageFormat {
794    #[default]
795    Rgb = ffi::KittyImageFormat::RGB,
796    Rgba = ffi::KittyImageFormat::RGBA,
797    Png = ffi::KittyImageFormat::PNG,
798    GrayAlpha = ffi::KittyImageFormat::GRAY_ALPHA,
799    Gray = ffi::KittyImageFormat::GRAY,
800}
801
802/// Compression of a Kitty graphics image.
803#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, int_enum::IntEnum)]
804#[non_exhaustive]
805#[repr(u32)]
806#[expect(missing_docs, reason = "missing upstream docs")]
807pub enum Compression {
808    #[default]
809    None = ffi::KittyImageCompression::NONE,
810    ZlibDeflate = ffi::KittyImageCompression::ZLIB_DEFLATE,
811}
812
813// Unlike other sys functions (e.g. `log::set_logger`), the decoder
814// callback will only ever be called on
815thread_local! {
816    static DECODE_PNG: RefCell<Option<Box<dyn DecodePng>>> = RefCell::new(None);
817}
818
819/// Set the PNG decoder.
820///
821/// When set, the terminal can accept PNG images via the Kitty Graphics Protocol.
822/// When cleared (`None` value), PNG decoding is unsupported and PNG image data
823/// will be rejected.
824///
825/// # Thread safety
826///
827/// This function must only be called on the same thread as the terminal
828pub fn set_png_decoder(f: Option<Box<dyn DecodePng>>) -> Result<()> {
829    unsafe extern "C" fn callback(
830        _userdata: *mut std::ffi::c_void,
831        allocator: *const ffi::Allocator,
832        data: *const u8,
833        data_len: usize,
834        out: *mut ffi::SysImage,
835    ) -> bool {
836        DECODE_PNG.with_borrow_mut(|decoder| {
837            let Some(decoder) = decoder else {
838                return false;
839            };
840            // SAFETY: We trust libghostty to return valid values.
841            let alloc = unsafe { Allocator::from_raw(allocator) };
842            let data = unsafe { std::slice::from_raw_parts(data, data_len) };
843
844            match decoder.decode_png(&alloc, data) {
845                Some(result) => {
846                    // IMPORTANT: Do NOT run the Rust destructor here
847                    // to avoid double-freeing the byte buffer.
848                    let mut result = ManuallyDrop::new(result);
849                    unsafe {
850                        *out = ffi::SysImage {
851                            width: result.width,
852                            height: result.height,
853                            data: result.data.as_mut_ptr(),
854                            data_len: result.data.len(),
855                        }
856                    };
857                    true
858                }
859                None => false,
860            }
861        })
862    }
863
864    // Write out the matches here to coerce function items into function
865    // pointers, and trait impls into boxed trait objects. Yes, this is
866    // the simplest way to do so.
867    let ptr: ffi::SysDecodePngFn = match f {
868        None => None,
869        Some(_) => Some(callback),
870    };
871    DECODE_PNG.replace(f);
872
873    crate::sys_set(
874        ffi::SysOption::GHOSTTY_SYS_OPT_DECODE_PNG,
875        ptr.map_or(std::ptr::null(), |p| p as *const std::ffi::c_void),
876    )
877}
878
879/// A PNG decoder that can be used by the Kitty graphics protocol
880/// to decode PNG images into 8-bit RGBA pixels.
881///
882/// See [`set_png_decoder`] for more details.
883pub trait DecodePng: 'static {
884    /// Decode a PNG into 8-bit RGBA pixels.
885    ///
886    /// The returned image's byte buffer *must* be allocated by
887    /// the provided allocator.
888    fn decode_png<'alloc>(
889        &mut self,
890        alloc: &'alloc Allocator<'_>,
891        data: &[u8],
892    ) -> Option<DecodedImage<'alloc>>;
893}
894
895/// A PNG decoder for [`set_png_decoder`] using the [`png`] crate.
896///
897/// ```rust
898/// use ghostty::kitty::graphics;
899///
900/// graphics::set_png_decoder(RustPngDecoder::new());
901/// ```
902#[cfg(all(feature = "kitty-graphics", feature = "png"))]
903#[derive(Clone, Debug)]
904pub struct RustPngDecoder {
905    buf: Vec<u8>,
906}
907#[cfg(all(feature = "kitty-graphics", feature = "png"))]
908impl DecodePng for RustPngDecoder {
909    fn decode_png<'alloc>(
910        &mut self,
911        alloc: &'alloc Allocator<'_>,
912        data: &[u8],
913    ) -> Option<DecodedImage<'alloc>> {
914        use png::{Decoder, Transformations};
915        use std::io::Cursor;
916
917        let mut decoder = Decoder::new(Cursor::new(data));
918
919        // libghostty only accepts RGBA8 data, so we have to apply some
920        // transformations to accept images in other formats, namely
921        // expanding palette and grayscale colors to RGBA8 and stripping
922        // 16-bit color depth information back down into 8-bit.
923        decoder.set_transformations(Transformations::ALPHA | Transformations::STRIP_16);
924
925        let mut frame = decoder.read_info().ok()?;
926        let buf_size = frame.output_buffer_size()?;
927        if buf_size > self.buf.capacity() {
928            self.buf.reserve(buf_size - self.buf.capacity());
929        }
930        self.buf.fill(0);
931
932        let info = frame.next_frame(&mut self.buf).ok()?;
933
934        let mut bytes = Bytes::new_with_alloc(alloc, info.buffer_size()).ok()?;
935        bytes.copy_from_slice(&self.buf[..info.buffer_size()]);
936        frame.finish().ok()?;
937
938        Some(DecodedImage {
939            width: info.width,
940            height: info.height,
941            data: bytes,
942        })
943    }
944}
945
946/// Result of decoding an image.
947///
948/// The `data` buffer must be allocated through the allocator provided to the
949/// decode callback. The library takes ownership and will free it with the
950/// same allocator.
951#[derive(Debug)]
952pub struct DecodedImage<'alloc> {
953    /// Image width in pixels.
954    pub width: u32,
955    /// Image height in pixels.
956    pub height: u32,
957    /// Byte buffer containing the decoded RGBA pixel data.
958    pub data: Bytes<'alloc>,
959}
960impl From<DecodedImage<'_>> for ffi::SysImage {
961    fn from(mut value: DecodedImage<'_>) -> Self {
962        Self {
963            width: value.width,
964            height: value.height,
965            data: value.data.as_mut_ptr(),
966            data_len: value.data.len(),
967        }
968    }
969}