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    /// 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}