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.
This commit is contained in:
Mitchell Hashimoto
2026-04-06 09:24:52 -07:00
parent 9033f6f8ce
commit 46a69ea63d
4 changed files with 319 additions and 0 deletions

View File

@@ -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.
*

View File

@@ -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" });

View File

@@ -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)));
}

View File

@@ -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;