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}