mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 11:35:48 +00:00
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:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user