From 46a69ea63d2891eca2e404eddd1bfbd84c66de0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 09:24:52 -0700 Subject: [PATCH] libghostty: add kitty graphics image lookup API Add a GhosttyKittyGraphicsImage opaque type and API for looking up images by ID and querying their properties. This complements the existing placement iterator by allowing direct image introspection. The new ghostty_kitty_graphics_image() function looks up an image by its ID from the storage, returning a borrowed opaque handle. Properties are queried via ghostty_kitty_image_get() using the new GhosttyKittyGraphicsImageData enum, which exposes id, number, width, height, format, compression, and a borrowed data pointer with length. Format and compression are exposed as their own C enum types (GhosttyKittyImageFormat and GhosttyKittyImageCompression) rather than raw integers. --- include/ghostty/vt/kitty_graphics.h | 135 +++++++++++++++++++++ src/lib_vt.zig | 2 + src/terminal/c/kitty_graphics.zig | 180 ++++++++++++++++++++++++++++ src/terminal/c/main.zig | 2 + 4 files changed, 319 insertions(+) diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h index df95b3a09..359e17ddc 100644 --- a/include/ghostty/vt/kitty_graphics.h +++ b/include/ghostty/vt/kitty_graphics.h @@ -36,6 +36,17 @@ extern "C" { */ typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics; +/** + * Opaque handle to a Kitty graphics image. + * + * Obtained via ghostty_kitty_graphics_image() with an image ID. The + * pointer is borrowed from the storage and remains valid until the next + * mutating terminal call. + * + * @ingroup kitty_graphics + */ +typedef const struct GhosttyKittyGraphicsImageImpl* GhosttyKittyGraphicsImage; + /** * Opaque handle to a Kitty graphics placement iterator. * @@ -156,6 +167,96 @@ typedef enum { GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12, } GhosttyKittyGraphicsPlacementData; +/** + * Pixel format of a Kitty graphics image. + * + * @ingroup kitty_graphics + */ +typedef enum { + GHOSTTY_KITTY_IMAGE_FORMAT_RGB = 0, + GHOSTTY_KITTY_IMAGE_FORMAT_RGBA = 1, + GHOSTTY_KITTY_IMAGE_FORMAT_PNG = 2, + GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA = 3, + GHOSTTY_KITTY_IMAGE_FORMAT_GRAY = 4, +} GhosttyKittyImageFormat; + +/** + * Compression of a Kitty graphics image. + * + * @ingroup kitty_graphics + */ +typedef enum { + GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE = 0, + GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE = 1, +} GhosttyKittyImageCompression; + +/** + * Queryable data kinds for ghostty_kitty_image_get(). + * + * @ingroup kitty_graphics + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_KITTY_IMAGE_DATA_INVALID = 0, + + /** + * The image ID. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_ID = 1, + + /** + * The image number. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_NUMBER = 2, + + /** + * Image width in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_WIDTH = 3, + + /** + * Image height in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_HEIGHT = 4, + + /** + * Pixel format of the image. + * + * Output type: GhosttyKittyImageFormat * + */ + GHOSTTY_KITTY_IMAGE_DATA_FORMAT = 5, + + /** + * Compression of the image. + * + * Output type: GhosttyKittyImageCompression * + */ + GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION = 6, + + /** + * Borrowed pointer to the raw pixel data. Valid as long as the + * underlying terminal is not mutated. + * + * Output type: const uint8_t ** + */ + GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR = 7, + + /** + * Length of the raw pixel data in bytes. + * + * Output type: size_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN = 8, +} GhosttyKittyGraphicsImageData; + /** * Get data from a kitty graphics storage instance. * @@ -176,6 +277,40 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get( GhosttyKittyGraphicsData data, void* out); +/** + * Look up a Kitty graphics image by its image ID. + * + * Returns NULL if no image with the given ID exists or if Kitty graphics + * are disabled at build time. + * + * @param graphics The kitty graphics handle + * @param image_id The image ID to look up + * @return An opaque image handle, or NULL if not found + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image( + GhosttyKittyGraphics graphics, + uint32_t image_id); + +/** + * Get data from a Kitty graphics image. + * + * The output pointer must be of the appropriate type for the requested + * data kind. + * + * @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_image_get( + GhosttyKittyGraphicsImage image, + GhosttyKittyGraphicsImageData data, + void* out); + /** * Create a new placement iterator instance. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index a009da01e..da113e4f7 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -234,6 +234,8 @@ comptime { @export(&c.terminal_get, .{ .name = "ghostty_terminal_get" }); @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); @export(&c.kitty_graphics_get, .{ .name = "ghostty_kitty_graphics_get" }); + @export(&c.kitty_graphics_image, .{ .name = "ghostty_kitty_graphics_image" }); + @export(&c.kitty_image_get, .{ .name = "ghostty_kitty_image_get" }); @export(&c.kitty_graphics_placement_iterator_new, .{ .name = "ghostty_kitty_graphics_placement_iterator_new" }); @export(&c.kitty_graphics_placement_iterator_free, .{ .name = "ghostty_kitty_graphics_placement_iterator_free" }); @export(&c.kitty_graphics_placement_next, .{ .name = "ghostty_kitty_graphics_placement_next" }); diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index 39f6c1f8f..bad8b6130 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -3,6 +3,7 @@ const build_options = @import("terminal_options"); const lib = @import("../lib.zig"); const CAllocator = lib.alloc.Allocator; const kitty_gfx = @import("../kitty/graphics_storage.zig"); +const Image = @import("../kitty/graphics_image.zig").Image; const Result = @import("result.zig").Result; /// C: GhosttyKittyGraphics @@ -11,6 +12,12 @@ pub const KittyGraphics = if (build_options.kitty_graphics) else *anyopaque; +/// C: GhosttyKittyGraphicsImage +pub const ImageHandle = if (build_options.kitty_graphics) + ?*const Image +else + ?*const anyopaque; + /// C: GhosttyKittyGraphicsPlacementIterator pub const PlacementIterator = ?*PlacementIteratorWrapper; @@ -109,6 +116,103 @@ fn getTyped( return .success; } +/// C: GhosttyKittyImageFormat +pub const ImageFormat = enum(c_int) { + rgb = 0, + rgba = 1, + png = 2, + gray_alpha = 3, + gray = 4, +}; + +/// C: GhosttyKittyImageCompression +pub const ImageCompression = enum(c_int) { + none = 0, + zlib_deflate = 1, +}; + +/// C: GhosttyKittyGraphicsImageData +pub const ImageData = enum(c_int) { + invalid = 0, + id = 1, + number = 2, + width = 3, + height = 4, + format = 5, + compression = 6, + data_ptr = 7, + data_len = 8, + + pub fn OutType(comptime self: ImageData) type { + return switch (self) { + .invalid => void, + .id, .number, .width, .height => u32, + .format => ImageFormat, + .compression => ImageCompression, + .data_ptr => [*]const u8, + .data_len => usize, + }; + } +}; + +pub fn image_get_handle( + graphics_: KittyGraphics, + image_id: u32, +) callconv(lib.calling_conv) ImageHandle { + if (comptime !build_options.kitty_graphics) return null; + + const storage = graphics_; + return storage.images.getPtr(image_id); +} + +pub fn image_get( + image_: ImageHandle, + data: ImageData, + out: ?*anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime !build_options.kitty_graphics) return .no_value; + + return switch (data) { + .invalid => .invalid_value, + inline else => |comptime_data| imageGetTyped( + image_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn imageGetTyped( + image_: ImageHandle, + comptime data: ImageData, + out: *data.OutType(), +) Result { + const image = image_ orelse return .invalid_value; + + switch (data) { + .invalid => return .invalid_value, + .id => out.* = image.id, + .number => out.* = image.number, + .width => out.* = image.width, + .height => out.* = image.height, + .format => out.* = switch (image.format) { + .rgb => .rgb, + .rgba => .rgba, + .png => .png, + .gray_alpha => .gray_alpha, + .gray => .gray, + }, + .compression => out.* = switch (image.compression) { + .none => .none, + .zlib_deflate => .zlib_deflate, + }, + .data_ptr => out.* = image.data.ptr, + .data_len => out.* = image.data.len, + } + + return .success; +} + pub fn placement_iterator_new( alloc_: ?*const CAllocator, out: *PlacementIterator, @@ -362,3 +466,79 @@ test "placement_iterator with multiple placements" { try testing.expect(seen_p1); try testing.expect(seen_p2); } + +test "image_get_handle returns null for missing id" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get( + t, + .kitty_graphics, + @ptrCast(&graphics), + )); + + try testing.expectEqual(@as(ImageHandle, null), image_get_handle(graphics, 999)); +} + +test "image_get_handle and image_get with transmitted image" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + + // Transmit a 1x2 RGB image with image_id=1. + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2;////////\x1b\\"; + terminal_c.vt_write(t, cmd.ptr, cmd.len); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get( + t, + .kitty_graphics, + @ptrCast(&graphics), + )); + + const img = image_get_handle(graphics, 1); + try testing.expect(img != null); + + var id: u32 = undefined; + try testing.expectEqual(Result.success, image_get(img, .id, @ptrCast(&id))); + try testing.expectEqual(1, id); + + var w: u32 = undefined; + try testing.expectEqual(Result.success, image_get(img, .width, @ptrCast(&w))); + try testing.expectEqual(1, w); + + var h: u32 = undefined; + try testing.expectEqual(Result.success, image_get(img, .height, @ptrCast(&h))); + try testing.expectEqual(2, h); + + var fmt: ImageFormat = undefined; + try testing.expectEqual(Result.success, image_get(img, .format, @ptrCast(&fmt))); + try testing.expectEqual(.rgba, fmt); + + var comp: ImageCompression = undefined; + try testing.expectEqual(Result.success, image_get(img, .compression, @ptrCast(&comp))); + try testing.expectEqual(.none, comp); + + var data_len: usize = undefined; + try testing.expectEqual(Result.success, image_get(img, .data_len, @ptrCast(&data_len))); + try testing.expect(data_len > 0); +} + +test "image_get on null returns invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var id: u32 = undefined; + try testing.expectEqual(Result.invalid_value, image_get(null, .id, @ptrCast(&id))); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 802ab72c3..c6e11c5e8 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -10,6 +10,8 @@ pub const formatter = @import("formatter.zig"); pub const grid_ref = @import("grid_ref.zig"); pub const kitty_graphics = @import("kitty_graphics.zig"); pub const kitty_graphics_get = kitty_graphics.get; +pub const kitty_graphics_image = kitty_graphics.image_get_handle; +pub const kitty_image_get = kitty_graphics.image_get; pub const kitty_graphics_placement_iterator_new = kitty_graphics.placement_iterator_new; pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iterator_free; pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next;