From e89b2c88f3a07956bd02bbd8279ead3bcbdd03a4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 08:49:40 -0700 Subject: [PATCH 01/11] libghostty: introduce the kitty graphics opaque type --- include/ghostty/vt.h | 1 + include/ghostty/vt/kitty_graphics.h | 40 +++++++++++++++++++++++++++++ include/ghostty/vt/terminal.h | 14 ++++++++++ src/terminal/c/kitty_graphics.zig | 8 ++++++ src/terminal/c/main.zig | 2 ++ src/terminal/c/terminal.zig | 10 ++++++++ 6 files changed, 75 insertions(+) create mode 100644 include/ghostty/vt/kitty_graphics.h create mode 100644 src/terminal/c/kitty_graphics.zig diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 5dd06521c..649ab1d4d 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -125,6 +125,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h new file mode 100644 index 000000000..aaacb5330 --- /dev/null +++ b/include/ghostty/vt/kitty_graphics.h @@ -0,0 +1,40 @@ +/** + * @file kitty_graphics.h + * + * Kitty graphics protocol image storage. + */ + +#ifndef GHOSTTY_VT_KITTY_GRAPHICS_H +#define GHOSTTY_VT_KITTY_GRAPHICS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup kitty_graphics Kitty Graphics + * + * Opaque handle to the Kitty graphics image storage associated with a + * terminal screen. + * + * @{ + */ + +/** + * Opaque handle to a Kitty graphics image storage. + * + * Obtained via ghostty_terminal_get() with + * GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from + * the terminal and remains valid until the next mutating terminal call + * (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()). + * + * @ingroup kitty_graphics + */ +typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics; + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_KITTY_GRAPHICS_H */ diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index c243fa25c..ff3f60ae1 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -839,6 +840,19 @@ typedef enum { * Output type: bool * */ GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM = 29, + + /** + * The Kitty graphics image storage for the active screen. + * + * Returns a borrowed pointer to the image storage. The pointer is valid + * until the next mutating terminal call (e.g. ghostty_terminal_vt_write() + * or ghostty_terminal_reset()). + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: GhosttyKittyGraphics * + */ + GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30, } GhosttyTerminalData; /** diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig new file mode 100644 index 000000000..cc0834859 --- /dev/null +++ b/src/terminal/c/kitty_graphics.zig @@ -0,0 +1,8 @@ +const build_options = @import("terminal_options"); +const kitty_gfx = @import("../kitty/graphics_storage.zig"); + +/// C: GhosttyKittyGraphics +pub const KittyGraphics = if (build_options.kitty_graphics) + *kitty_gfx.ImageStorage +else + *anyopaque; diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 997a8e2c8..ef678e438 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -8,6 +8,7 @@ pub const color = @import("color.zig"); pub const focus = @import("focus.zig"); pub const formatter = @import("formatter.zig"); pub const grid_ref = @import("grid_ref.zig"); +pub const kitty_graphics = @import("kitty_graphics.zig"); pub const types = @import("types.zig"); pub const modes = @import("modes.zig"); pub const osc = @import("osc.zig"); @@ -161,6 +162,7 @@ test { _ = cell; _ = color; _ = grid_ref; + _ = kitty_graphics; _ = row; _ = focus; _ = formatter; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index a2b0d1092..32bc0311a 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -8,6 +8,7 @@ const Stream = @import("../stream_terminal.zig").Stream; const ScreenSet = @import("../ScreenSet.zig"); const PageList = @import("../PageList.zig"); const kitty = @import("../kitty/key.zig"); +const kitty_gfx_c = @import("kitty_graphics.zig"); const modes = @import("../modes.zig"); const point = @import("../point.zig"); const size = @import("../size.zig"); @@ -515,6 +516,9 @@ pub fn mode_set( return .success; } +/// C: GhosttyKittyGraphics +pub const KittyGraphics = kitty_gfx_c.KittyGraphics; + /// C: GhosttyTerminalScreen pub const TerminalScreen = ScreenSet.Key; @@ -553,6 +557,7 @@ pub const TerminalData = enum(c_int) { kitty_image_medium_file = 27, kitty_image_medium_temp_file = 28, kitty_image_medium_shared_mem = 29, + kitty_graphics = 30, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { @@ -580,6 +585,7 @@ pub const TerminalData = enum(c_int) { .kitty_image_medium_temp_file, .kitty_image_medium_shared_mem, => bool, + .kitty_graphics => KittyGraphics, }; } }; @@ -664,6 +670,10 @@ fn getTyped( if (comptime !build_options.kitty_graphics) return .no_value; out.* = t.screens.active.kitty_images.image_limits.shared_memory; }, + .kitty_graphics => { + if (comptime !build_options.kitty_graphics) return .no_value; + out.* = &t.screens.active.kitty_images; + }, } return .success; From 9033f6f8ce2bc50fb2529616764fcb2325ae67b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 09:06:06 -0700 Subject: [PATCH 02/11] libghostty: add kitty graphics placement iterator API Add a C API for iterating over Kitty graphics placements via the new GhosttyKittyGraphics opaque handle. The API follows the same pattern as the render state row iterator: allocate an iterator with ghostty_kitty_graphics_placement_iterator_new, populate it from a graphics handle via ghostty_kitty_graphics_get with the PLACEMENT_ITERATOR data kind, advance with ghostty_kitty_graphics_placement_next, and query per-placement fields with ghostty_kitty_graphics_placement_get. --- include/ghostty/vt/kitty_graphics.h | 205 +++++++++++++++- src/lib_vt.zig | 5 + src/terminal/c/AGENTS.md | 7 +- src/terminal/c/kitty_graphics.zig | 356 ++++++++++++++++++++++++++++ src/terminal/c/main.zig | 5 + 5 files changed, 576 insertions(+), 2 deletions(-) diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h index aaacb5330..df95b3a09 100644 --- a/include/ghostty/vt/kitty_graphics.h +++ b/include/ghostty/vt/kitty_graphics.h @@ -7,6 +7,11 @@ #ifndef GHOSTTY_VT_KITTY_GRAPHICS_H #define GHOSTTY_VT_KITTY_GRAPHICS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,7 +19,7 @@ extern "C" { /** @defgroup kitty_graphics Kitty Graphics * * Opaque handle to the Kitty graphics image storage associated with a - * terminal screen. + * terminal screen, and an iterator for inspecting placements. * * @{ */ @@ -31,6 +36,204 @@ extern "C" { */ typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics; +/** + * Opaque handle to a Kitty graphics placement iterator. + * + * @ingroup kitty_graphics + */ +typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator; + +/** + * Queryable data kinds for ghostty_kitty_graphics_get(). + * + * @ingroup kitty_graphics + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_KITTY_GRAPHICS_DATA_INVALID = 0, + + /** + * Populate a pre-allocated placement iterator with placement data from + * the storage. Iterator data is only valid as long as the underlying + * terminal is not mutated. + * + * Output type: GhosttyKittyGraphicsPlacementIterator * + */ + GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR = 1, +} GhosttyKittyGraphicsData; + +/** + * Queryable data kinds for ghostty_kitty_graphics_placement_get(). + * + * @ingroup kitty_graphics + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INVALID = 0, + + /** + * The image ID this placement belongs to. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID = 1, + + /** + * The placement ID. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID = 2, + + /** + * Whether this is a virtual placement (unicode placeholder). + * + * Output type: bool * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL = 3, + + /** + * Pixel offset from the left edge of the cell. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET = 4, + + /** + * Pixel offset from the top edge of the cell. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET = 5, + + /** + * Source rectangle x origin in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X = 6, + + /** + * Source rectangle y origin in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y = 7, + + /** + * Source rectangle width in pixels (0 = full image width). + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH = 8, + + /** + * Source rectangle height in pixels (0 = full image height). + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT = 9, + + /** + * Number of columns this placement occupies. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS = 10, + + /** + * Number of rows this placement occupies. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS = 11, + + /** + * Z-index for this placement. + * + * Output type: int32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12, +} GhosttyKittyGraphicsPlacementData; + +/** + * Get data from a kitty graphics storage instance. + * + * The output pointer must be of the appropriate type for the requested + * data kind. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * @param graphics The kitty graphics handle + * @param data The type of data to extract + * @param[out] out Pointer to store the extracted data + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get( + GhosttyKittyGraphics graphics, + GhosttyKittyGraphicsData data, + void* out); + +/** + * Create a new placement iterator instance. + * + * All fields except the allocator are left undefined until populated + * via ghostty_kitty_graphics_get() with + * GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_iterator On success, receives the created iterator handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_new( + const GhosttyAllocator* allocator, + GhosttyKittyGraphicsPlacementIterator* out_iterator); + +/** + * Free a placement iterator. + * + * @param iterator The iterator handle to free (may be NULL) + * + * @ingroup kitty_graphics + */ +GHOSTTY_API void ghostty_kitty_graphics_placement_iterator_free( + GhosttyKittyGraphicsPlacementIterator iterator); + +/** + * Advance the placement iterator to the next placement. + * + * @param iterator The iterator handle (may be NULL) + * @return true if advanced to the next placement, false if at the end + * + * @ingroup kitty_graphics + */ +GHOSTTY_API bool ghostty_kitty_graphics_placement_next( + GhosttyKittyGraphicsPlacementIterator iterator); + +/** + * Get data from the current placement in a placement iterator. + * + * Call ghostty_kitty_graphics_placement_next() at least once before + * calling this function. + * + * @param iterator The iterator 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, GHOSTTY_INVALID_VALUE if the + * iterator is NULL or not positioned on a placement + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsPlacementData data, + void* out); + /** @} */ #ifdef __cplusplus diff --git a/src/lib_vt.zig b/src/lib_vt.zig index deee9633c..a009da01e 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -233,6 +233,11 @@ comptime { @export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" }); @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_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" }); + @export(&c.kitty_graphics_placement_get, .{ .name = "ghostty_kitty_graphics_placement_get" }); @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); diff --git a/src/terminal/c/AGENTS.md b/src/terminal/c/AGENTS.md index 63f7fc6cc..c7e9068a8 100644 --- a/src/terminal/c/AGENTS.md +++ b/src/terminal/c/AGENTS.md @@ -5,7 +5,12 @@ via `lib.TaggedUnion`. - Any functions must be updated all the way through from here to `src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers - in `include/ghostty/vt.h`. + in `include/ghostty/vt.h`. Specifically: + 1. Define the function in `src/terminal/c/.zig`. + 2. Re-export it via a `pub const` in `src/terminal/c/main.zig`. + 3. Add an `@export` call in `src/lib_vt.zig` with the + `ghostty_` prefixed symbol name. + 4. Declare it in the corresponding header under `include/ghostty/vt/`. - In `include/ghostty/vt.h`, always sort the header contents by: (1) macros, (2) forward declarations, (3) types, (4) functions diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index cc0834859..39f6c1f8f 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -1,8 +1,364 @@ +const std = @import("std"); 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 Result = @import("result.zig").Result; /// C: GhosttyKittyGraphics pub const KittyGraphics = if (build_options.kitty_graphics) *kitty_gfx.ImageStorage else *anyopaque; + +/// C: GhosttyKittyGraphicsPlacementIterator +pub const PlacementIterator = ?*PlacementIteratorWrapper; + +const PlacementMap = std.AutoHashMapUnmanaged( + kitty_gfx.ImageStorage.PlacementKey, + kitty_gfx.ImageStorage.Placement, +); + +const PlacementIteratorWrapper = struct { + alloc: std.mem.Allocator, + inner: PlacementMap.Iterator = undefined, + entry: ?PlacementMap.Entry = null, +}; + +/// C: GhosttyKittyGraphicsData +pub const Data = enum(c_int) { + invalid = 0, + placement_iterator = 1, + + pub fn OutType(comptime self: Data) type { + return switch (self) { + .invalid => void, + .placement_iterator => PlacementIterator, + }; + } +}; + +/// C: GhosttyKittyGraphicsPlacementData +pub const PlacementData = enum(c_int) { + invalid = 0, + image_id = 1, + placement_id = 2, + is_virtual = 3, + x_offset = 4, + y_offset = 5, + source_x = 6, + source_y = 7, + source_width = 8, + source_height = 9, + columns = 10, + rows = 11, + z = 12, + + pub fn OutType(comptime self: PlacementData) type { + return switch (self) { + .invalid => void, + .image_id, .placement_id => u32, + .is_virtual => bool, + .x_offset, + .y_offset, + .source_x, + .source_y, + .source_width, + .source_height, + .columns, + .rows, + => u32, + .z => i32, + }; + } +}; + +pub fn get( + graphics_: KittyGraphics, + data: Data, + 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| getTyped( + graphics_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn getTyped( + graphics_: KittyGraphics, + comptime data: Data, + out: *data.OutType(), +) Result { + const storage = graphics_; + switch (data) { + .invalid => return .invalid_value, + .placement_iterator => { + const it = out.* orelse return .invalid_value; + it.* = .{ + .alloc = it.alloc, + .inner = storage.placements.iterator(), + }; + }, + } + return .success; +} + +pub fn placement_iterator_new( + alloc_: ?*const CAllocator, + out: *PlacementIterator, +) callconv(lib.calling_conv) Result { + const alloc = lib.alloc.default(alloc_); + const ptr = alloc.create(PlacementIteratorWrapper) catch { + out.* = null; + return .out_of_memory; + }; + ptr.* = .{ .alloc = alloc }; + out.* = ptr; + return .success; +} + +pub fn placement_iterator_free(iter_: PlacementIterator) callconv(lib.calling_conv) void { + const iter = iter_ orelse return; + iter.alloc.destroy(iter); +} + +pub fn placement_iterator_next(iter_: PlacementIterator) callconv(lib.calling_conv) bool { + if (comptime !build_options.kitty_graphics) return false; + + const iter = iter_ orelse return false; + iter.entry = iter.inner.next() orelse return false; + return true; +} + +pub fn placement_get( + iter_: PlacementIterator, + data: PlacementData, + 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| placementGetTyped( + iter_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn placementGetTyped( + iter_: PlacementIterator, + comptime data: PlacementData, + out: *data.OutType(), +) Result { + const iter = iter_ orelse return .invalid_value; + const entry = iter.entry orelse return .invalid_value; + + switch (data) { + .invalid => return .invalid_value, + .image_id => out.* = entry.key_ptr.image_id, + .placement_id => out.* = entry.key_ptr.placement_id.id, + .is_virtual => out.* = entry.value_ptr.location == .virtual, + .x_offset => out.* = entry.value_ptr.x_offset, + .y_offset => out.* = entry.value_ptr.y_offset, + .source_x => out.* = entry.value_ptr.source_x, + .source_y => out.* = entry.value_ptr.source_y, + .source_width => out.* = entry.value_ptr.source_width, + .source_height => out.* = entry.value_ptr.source_height, + .columns => out.* = entry.value_ptr.columns, + .rows => out.* = entry.value_ptr.rows, + .z => out.* = entry.value_ptr.z, + } + + return .success; +} + +const testing = std.testing; +const terminal_c = @import("terminal.zig"); + +test "placement_iterator new/free" { + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + try testing.expect(iter != null); + placement_iterator_free(iter); +} + +test "placement_iterator free null" { + placement_iterator_free(null); +} + +test "placement_iterator next on empty storage" { + 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), + )); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(!placement_iterator_next(iter)); +} + +test "placement_iterator get before next returns invalid" { + 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), + )); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + + var image_id: u32 = undefined; + try testing.expectEqual(Result.invalid_value, placement_get(iter, .image_id, @ptrCast(&image_id))); +} + +test "placement_iterator with transmit and display" { + 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 and display a 1x2 RGB image (image_id=1, placement_id=1). + // a=T (transmit+display), t=d (direct), f=24 (RGB), i=1, p=1 + // s=1,v=2 (1x2 pixels), c=10,r=1 (10 cols, 1 row) + // //////// = 8 base64 chars = 6 bytes = 1*2*3 RGB bytes + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\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), + )); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + + // Should have exactly one placement. + try testing.expect(placement_iterator_next(iter)); + + var image_id: u32 = undefined; + try testing.expectEqual(Result.success, placement_get(iter, .image_id, @ptrCast(&image_id))); + try testing.expectEqual(1, image_id); + + var placement_id: u32 = undefined; + try testing.expectEqual(Result.success, placement_get(iter, .placement_id, @ptrCast(&placement_id))); + try testing.expectEqual(1, placement_id); + + var is_virtual: bool = undefined; + try testing.expectEqual(Result.success, placement_get(iter, .is_virtual, @ptrCast(&is_virtual))); + try testing.expect(!is_virtual); + + // No more placements. + try testing.expect(!placement_iterator_next(iter)); +} + +test "placement_iterator with multiple placements" { + 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 image 1 then display it twice with different placement IDs. + const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\"; + const display1 = "\x1b_Ga=p,i=1,p=1,c=10,r=1;\x1b\\"; + const display2 = "\x1b_Ga=p,i=1,p=2,c=5,r=1;\x1b\\"; + terminal_c.vt_write(t, transmit.ptr, transmit.len); + terminal_c.vt_write(t, display1.ptr, display1.len); + terminal_c.vt_write(t, display2.ptr, display2.len); + + var graphics: KittyGraphics = undefined; + try testing.expectEqual(Result.success, terminal_c.get( + t, + .kitty_graphics, + @ptrCast(&graphics), + )); + + var iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + + // Count placements and collect image IDs. + var count: usize = 0; + var seen_p1 = false; + var seen_p2 = false; + while (placement_iterator_next(iter)) { + count += 1; + + var image_id: u32 = undefined; + try testing.expectEqual(Result.success, placement_get(iter, .image_id, @ptrCast(&image_id))); + try testing.expectEqual(1, image_id); + + var placement_id: u32 = undefined; + try testing.expectEqual(Result.success, placement_get(iter, .placement_id, @ptrCast(&placement_id))); + if (placement_id == 1) seen_p1 = true; + if (placement_id == 2) seen_p2 = true; + } + + try testing.expectEqual(2, count); + try testing.expect(seen_p1); + try testing.expect(seen_p2); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index ef678e438..802ab72c3 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -9,6 +9,11 @@ pub const focus = @import("focus.zig"); 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_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; +pub const kitty_graphics_placement_get = kitty_graphics.placement_get; pub const types = @import("types.zig"); pub const modes = @import("modes.zig"); pub const osc = @import("osc.zig"); From 46a69ea63d2891eca2e404eddd1bfbd84c66de0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 09:24:52 -0700 Subject: [PATCH 03/11] 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; From 9ff4bb2df5d2542f7f4e189aebe309d907e3449e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 09:31:05 -0700 Subject: [PATCH 04/11] terminal/kitty: convert Format, Medium, Compression to lib.Enum Convert the Transmission.Format, Transmission.Medium, and Transmission.Compression types from plain Zig enums to lib.Enum so they get a C-compatible backing type when building with c_abi. This lets the C API layer reuse the types directly instead of maintaining separate mirror enums. Move Format.bpp() to a standalone Transmission.formatBpp() function since lib.Enum types cannot have decls. In the C API layer, rename kitty_gfx to kitty_storage and command to kitty_cmd for clarity, and simplify the format/compression getters to direct assignment now that the types are shared. --- src/terminal/c/kitty_graphics.zig | 53 ++++++++++-------------- src/terminal/kitty/graphics_command.zig | 54 ++++++++++++------------- src/terminal/kitty/graphics_image.zig | 6 +-- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index bad8b6130..ecfc574c2 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -2,13 +2,14 @@ const std = @import("std"); 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 kitty_storage = @import("../kitty/graphics_storage.zig"); +const kitty_cmd = @import("../kitty/graphics_command.zig"); const Image = @import("../kitty/graphics_image.zig").Image; const Result = @import("result.zig").Result; /// C: GhosttyKittyGraphics pub const KittyGraphics = if (build_options.kitty_graphics) - *kitty_gfx.ImageStorage + *kitty_storage.ImageStorage else *anyopaque; @@ -22,8 +23,8 @@ else pub const PlacementIterator = ?*PlacementIteratorWrapper; const PlacementMap = std.AutoHashMapUnmanaged( - kitty_gfx.ImageStorage.PlacementKey, - kitty_gfx.ImageStorage.Placement, + kitty_storage.ImageStorage.PlacementKey, + kitty_storage.ImageStorage.Placement, ); const PlacementIteratorWrapper = struct { @@ -117,19 +118,10 @@ fn getTyped( } /// C: GhosttyKittyImageFormat -pub const ImageFormat = enum(c_int) { - rgb = 0, - rgba = 1, - png = 2, - gray_alpha = 3, - gray = 4, -}; +pub const ImageFormat = kitty_cmd.Transmission.Format; /// C: GhosttyKittyImageCompression -pub const ImageCompression = enum(c_int) { - none = 0, - zlib_deflate = 1, -}; +pub const ImageCompression = kitty_cmd.Transmission.Compression; /// C: GhosttyKittyGraphicsImageData pub const ImageData = enum(c_int) { @@ -195,17 +187,8 @@ fn imageGetTyped( .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, - }, + .format => out.* = image.format, + .compression => out.* = image.compression, .data_ptr => out.* = image.data.ptr, .data_len => out.* = image.data.len, } @@ -306,7 +289,8 @@ test "placement_iterator next on empty storage" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( - &lib.alloc.test_allocator, &t, + &lib.alloc.test_allocator, + &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); @@ -334,7 +318,8 @@ test "placement_iterator get before next returns invalid" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( - &lib.alloc.test_allocator, &t, + &lib.alloc.test_allocator, + &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); @@ -364,7 +349,8 @@ test "placement_iterator with transmit and display" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( - &lib.alloc.test_allocator, &t, + &lib.alloc.test_allocator, + &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); @@ -416,7 +402,8 @@ test "placement_iterator with multiple placements" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( - &lib.alloc.test_allocator, &t, + &lib.alloc.test_allocator, + &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); @@ -472,7 +459,8 @@ test "image_get_handle returns null for missing id" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( - &lib.alloc.test_allocator, &t, + &lib.alloc.test_allocator, + &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); @@ -492,7 +480,8 @@ test "image_get_handle and image_get with transmitted image" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( - &lib.alloc.test_allocator, &t, + &lib.alloc.test_allocator, + &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index dfce56e35..d1f0e6b63 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -3,6 +3,7 @@ const assert = @import("../../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const simd = @import("../../simd/main.zig"); +const lib = @import("../lib.zig"); const log = std.log.scoped(.kitty_gfx); @@ -394,39 +395,38 @@ pub const Transmission = struct { compression: Compression = .none, // o more_chunks: bool = false, // m - pub const Format = enum { - rgb, // 24 - rgba, // 32 - png, // 100 - + pub const Format = lib.Enum(lib.target, &.{ + "rgb", // 24 + "rgba", // 32 + "png", // 100 // The following are not supported directly via the protocol // but they are formats that a png may decode to that we // support. - gray_alpha, - gray, + "gray_alpha", + "gray", + }); - pub fn bpp(self: Format) u8 { - return switch (self) { - .gray => 1, - .gray_alpha => 2, - .rgb => 3, - .rgba => 4, - .png => unreachable, // Must be validated before - }; - } - }; + pub const Medium = lib.Enum(lib.target, &.{ + "direct", // d + "file", // f + "temporary_file", // t + "shared_memory", // s + }); - pub const Medium = enum { - direct, // d - file, // f - temporary_file, // t - shared_memory, // s - }; + pub const Compression = lib.Enum(lib.target, &.{ + "none", + "zlib_deflate", // z + }); - pub const Compression = enum { - none, - zlib_deflate, // z - }; + pub fn formatBpp(format: Format) u8 { + return switch (format) { + .gray => 1, + .gray_alpha => 2, + .rgb => 3, + .rgba => 4, + .png => unreachable, // Must be validated before + }; + } fn parse(kv: KV) !Transmission { var result: Transmission = .{}; diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index f1f055fa0..bddc5c5b2 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -202,8 +202,8 @@ pub const LoadingImage = struct { .png => stat_size, // For these formats we have a size we must have. - .gray, .gray_alpha, .rgb, .rgba => |f| size: { - const bpp = f.bpp(); + .gray, .gray_alpha, .rgb, .rgba => size: { + const bpp = command.Transmission.formatBpp(self.image.format); break :size self.image.width * self.image.height * bpp; }, }; @@ -390,7 +390,7 @@ pub const LoadingImage = struct { if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge; // Data length must be what we expect - const bpp = img.format.bpp(); + const bpp = command.Transmission.formatBpp(img.format); const expected_len = img.width * img.height * bpp; const actual_len = self.data.items.len; if (actual_len != expected_len) { From 714420409be233bb0acacdc60f6d15f6822de8e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 09:52:49 -0700 Subject: [PATCH 05/11] libghostty: add placement_rect and centralize opaque typedefs Expose Placement.rect() from the Zig kitty graphics storage as a new C API function ghostty_kitty_graphics_placement_rect(). It takes the terminal, image handle, and a positioned placement iterator, and writes the bounding grid rectangle into a GhosttySelection out param. Virtual placements return GHOSTTY_NO_VALUE. Move all opaque handle typedefs (GhosttyTerminal, GhosttyKittyGraphics, GhosttyRenderState, GhosttySgrParser, GhosttyFormatter, GhosttyOsc*) into types.h so they are available everywhere without circular includes and Doxygen renders them in the correct @ingroup sections. --- include/ghostty/vt/formatter.h | 7 -- include/ghostty/vt/kitty_graphics.h | 55 ++++++++-------- include/ghostty/vt/osc.h | 20 ------ include/ghostty/vt/render.h | 21 ------ include/ghostty/vt/sgr.h | 10 --- include/ghostty/vt/terminal.h | 7 -- include/ghostty/vt/types.h | 99 +++++++++++++++++++++++++++++ src/lib_vt.zig | 1 + src/terminal/c/kitty_graphics.zig | 92 ++++++++++++++++++++++++++- src/terminal/c/main.zig | 1 + 10 files changed, 216 insertions(+), 97 deletions(-) diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h index 19f6664c3..9eacc6409 100644 --- a/include/ghostty/vt/formatter.h +++ b/include/ghostty/vt/formatter.h @@ -107,13 +107,6 @@ typedef struct { GhosttyFormatterScreenExtra screen; } GhosttyFormatterTerminalExtra; -/** - * Opaque handle to a formatter instance. - * - * @ingroup formatter - */ -typedef struct GhosttyFormatterImpl* GhosttyFormatter; - /** * Options for creating a terminal formatter. * diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h index 359e17ddc..25f3128e1 100644 --- a/include/ghostty/vt/kitty_graphics.h +++ b/include/ghostty/vt/kitty_graphics.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #ifdef __cplusplus @@ -24,36 +25,6 @@ extern "C" { * @{ */ -/** - * Opaque handle to a Kitty graphics image storage. - * - * Obtained via ghostty_terminal_get() with - * GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from - * the terminal and remains valid until the next mutating terminal call - * (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()). - * - * @ingroup kitty_graphics - */ -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. - * - * @ingroup kitty_graphics - */ -typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator; - /** * Queryable data kinds for ghostty_kitty_graphics_get(). * @@ -369,6 +340,30 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get( GhosttyKittyGraphicsPlacementData data, void* out); +/** + * Compute the grid rectangle occupied by the current placement. + * + * Uses the placement's pin, the image dimensions, and the terminal's + * cell/pixel geometry to calculate the bounding rectangle. Virtual + * placements (unicode placeholders) return GHOSTTY_NO_VALUE. + * + * @param terminal The terminal handle + * @param image The image handle for this placement's image + * @param iterator The placement iterator positioned on a placement + * @param[out] out_selection On success, receives the bounding rectangle + * as a selection with rectangle=true + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE for + * virtual placements or when Kitty graphics are disabled + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + GhosttySelection* out_selection); + /** @} */ #ifdef __cplusplus diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index c86498090..e17a8a182 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -13,26 +13,6 @@ #include #include -/** - * Opaque handle to an OSC parser instance. - * - * This handle represents an OSC (Operating System Command) parser that can - * be used to parse the contents of OSC sequences. - * - * @ingroup osc - */ -typedef struct GhosttyOscParserImpl *GhosttyOscParser; - -/** - * Opaque handle to a single OSC command. - * - * This handle represents a parsed OSC (Operating System Command) command. - * The command can be queried for its type and associated data. - * - * @ingroup osc - */ -typedef struct GhosttyOscCommandImpl *GhosttyOscCommand; - /** @defgroup osc OSC Parser * * OSC (Operating System Command) sequence parser and command handling. diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index 163a4e1d4..b15be4902 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -81,27 +81,6 @@ extern "C" { * @{ */ -/** - * Opaque handle to a render state instance. - * - * @ingroup render - */ -typedef struct GhosttyRenderStateImpl* GhosttyRenderState; - -/** - * Opaque handle to a render-state row iterator. - * - * @ingroup render - */ -typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator; - -/** - * Opaque handle to render-state row cells. - * - * @ingroup render - */ -typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells; - /** * Dirty state of a render state after update. * diff --git a/include/ghostty/vt/sgr.h b/include/ghostty/vt/sgr.h index 01ea3a359..b093bc9ff 100644 --- a/include/ghostty/vt/sgr.h +++ b/include/ghostty/vt/sgr.h @@ -47,16 +47,6 @@ extern "C" { #endif -/** - * Opaque handle to an SGR parser instance. - * - * This handle represents an SGR (Select Graphic Rendition) parser that can - * be used to parse SGR sequences and extract individual text attributes. - * - * @ingroup sgr - */ -typedef struct GhosttySgrParserImpl* GhosttySgrParser; - /** * SGR attribute tags. * diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index ff3f60ae1..73db8d6d1 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -155,13 +155,6 @@ extern "C" { * @{ */ -/** - * Opaque handle to a terminal instance. - * - * @ingroup terminal - */ -typedef struct GhosttyTerminalImpl* GhosttyTerminal; - /** * Terminal initialization options. * diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h index 8f0be7760..0fe37e3b2 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h @@ -48,6 +48,105 @@ typedef enum { GHOSTTY_NO_VALUE = -4, } GhosttyResult; +/* ---- Opaque handles ---- */ + +/** + * Opaque handle to a terminal instance. + * + * @ingroup terminal + */ +typedef struct GhosttyTerminalImpl* GhosttyTerminal; + +/** + * Opaque handle to a Kitty graphics image storage. + * + * Obtained via ghostty_terminal_get() with + * GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from + * the terminal and remains valid until the next mutating terminal call + * (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()). + * + * @ingroup kitty_graphics + */ +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. + * + * @ingroup kitty_graphics + */ +typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator; + +/** + * Opaque handle to a render state instance. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateImpl* GhosttyRenderState; + +/** + * Opaque handle to a render-state row iterator. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator; + +/** + * Opaque handle to render-state row cells. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells; + +/** + * Opaque handle to an SGR parser instance. + * + * This handle represents an SGR (Select Graphic Rendition) parser that can + * be used to parse SGR sequences and extract individual text attributes. + * + * @ingroup sgr + */ +typedef struct GhosttySgrParserImpl* GhosttySgrParser; + +/** + * Opaque handle to a formatter instance. + * + * @ingroup formatter + */ +typedef struct GhosttyFormatterImpl* GhosttyFormatter; + +/** + * Opaque handle to an OSC parser instance. + * + * This handle represents an OSC (Operating System Command) parser that can + * be used to parse the contents of OSC sequences. + * + * @ingroup osc + */ +typedef struct GhosttyOscParserImpl* GhosttyOscParser; + +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data. + * + * @ingroup osc + */ +typedef struct GhosttyOscCommandImpl* GhosttyOscCommand; + +/* ---- Common value types ---- */ + /** * A borrowed byte string (pointer + length). * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index da113e4f7..9098f9dbc 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -240,6 +240,7 @@ comptime { @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" }); @export(&c.kitty_graphics_placement_get, .{ .name = "ghostty_kitty_graphics_placement_get" }); + @export(&c.kitty_graphics_placement_rect, .{ .name = "ghostty_kitty_graphics_placement_rect" }); @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index ecfc574c2..70ad5f818 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -1,10 +1,14 @@ const std = @import("std"); +const testing = std.testing; const build_options = @import("terminal_options"); const lib = @import("../lib.zig"); const CAllocator = lib.alloc.Allocator; const kitty_storage = @import("../kitty/graphics_storage.zig"); const kitty_cmd = @import("../kitty/graphics_command.zig"); const Image = @import("../kitty/graphics_image.zig").Image; +const grid_ref = @import("grid_ref.zig"); +const selection_c = @import("selection.zig"); +const terminal_c = @import("terminal.zig"); const Result = @import("result.zig").Result; /// C: GhosttyKittyGraphics @@ -267,8 +271,31 @@ fn placementGetTyped( return .success; } -const testing = std.testing; -const terminal_c = @import("terminal.zig"); +pub fn placement_rect( + iter_: PlacementIterator, + image_: ImageHandle, + terminal_: terminal_c.Terminal, + out: *selection_c.CSelection, +) callconv(lib.calling_conv) Result { + if (comptime !build_options.kitty_graphics) return .no_value; + + const wrapper = terminal_ orelse return .invalid_value; + const image = image_ orelse return .invalid_value; + const iter = iter_ orelse return .invalid_value; + const entry = iter.entry orelse return .invalid_value; + const r = entry.value_ptr.rect( + image.*, + wrapper.terminal, + ) orelse return .no_value; + + out.* = .{ + .start = grid_ref.CGridRef.fromPin(r.top_left), + .end = grid_ref.CGridRef.fromPin(r.bottom_right), + .rectangle = true, + }; + + return .success; +} test "placement_iterator new/free" { var iter: PlacementIterator = null; @@ -525,6 +552,67 @@ test "image_get_handle and image_get with transmitted image" { try testing.expect(data_len > 0); } +test "placement_rect with transmit and display" { + 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); + + // Set cell size so grid calculations are deterministic. + // 80 cols * 10px = 800px, 24 rows * 20px = 480px. + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); + + // Transmit and display a 1x2 RGB image at cursor (0,0). + // c=10,r=1 => 10 columns, 1 row. + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\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 iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(placement_iterator_next(iter)); + + var sel: selection_c.CSelection = undefined; + try testing.expectEqual(Result.success, placement_rect(iter, img, t, &sel)); + + // Placement starts at cursor origin (0,0). + try testing.expectEqual(0, sel.start.x); + try testing.expectEqual(0, sel.start.y); + + // 10 columns wide, 1 row tall => bottom-right is (9, 0). + try testing.expectEqual(9, sel.end.x); + try testing.expectEqual(0, sel.end.y); + + try testing.expect(sel.rectangle); +} + +test "placement_rect null args return invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var sel: selection_c.CSelection = undefined; + try testing.expectEqual(Result.invalid_value, placement_rect(null, null, null, &sel)); +} + test "image_get on null returns invalid_value" { if (comptime !build_options.kitty_graphics) return error.SkipZigTest; diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index c6e11c5e8..76c471dd0 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -16,6 +16,7 @@ pub const kitty_graphics_placement_iterator_new = kitty_graphics.placement_itera pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iterator_free; pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next; pub const kitty_graphics_placement_get = kitty_graphics.placement_get; +pub const kitty_graphics_placement_rect = kitty_graphics.placement_rect; pub const types = @import("types.zig"); pub const modes = @import("modes.zig"); pub const osc = @import("osc.zig"); From 03a6eeda1de9b00164c97960b176fe2b8457acb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:01:43 -0700 Subject: [PATCH 06/11] libghostty: add placement pixel_size and grid_size, rename calculatedSize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose Placement.pixelSize() and Placement.gridSize() as new C API functions ghostty_kitty_graphics_placement_pixel_size() and ghostty_kitty_graphics_placement_grid_size(). Both take the placement iterator, image handle, and terminal, returning their results via out params. Rename the internal Zig method from calculatedSize to pixelSize to pair naturally with gridSize — one returns pixels, the other grid cells. Updated all callers including the renderer. --- include/ghostty/vt/kitty_graphics.h | 51 ++++++++ src/lib_vt.zig | 2 + src/renderer/image.zig | 2 +- src/terminal/c/kitty_graphics.zig | 151 ++++++++++++++++++++++++ src/terminal/c/main.zig | 2 + src/terminal/kitty/graphics_storage.zig | 13 +- 6 files changed, 214 insertions(+), 7 deletions(-) diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h index 25f3128e1..124dc4265 100644 --- a/include/ghostty/vt/kitty_graphics.h +++ b/include/ghostty/vt/kitty_graphics.h @@ -364,6 +364,57 @@ GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect( GhosttyTerminal terminal, GhosttySelection* out_selection); +/** + * Compute the rendered pixel size of the current placement. + * + * Takes into account the placement's source rectangle, specified + * columns/rows, and aspect ratio to calculate the final rendered + * pixel dimensions. + * + * @param iterator The placement iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_width On success, receives the width in pixels + * @param[out] out_height On success, receives the height in pixels + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when + * Kitty graphics are disabled + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_pixel_size( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + uint32_t* out_width, + uint32_t* out_height); + +/** + * Compute the grid cell size of the current placement. + * + * Returns the number of columns and rows that the placement occupies + * in the terminal grid. If the placement specifies explicit columns + * and rows, those are returned directly; otherwise they are calculated + * from the pixel size and cell dimensions. + * + * @param iterator The placement iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_cols On success, receives the number of columns + * @param[out] out_rows On success, receives the number of rows + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when + * Kitty graphics are disabled + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_grid_size( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + uint32_t* out_cols, + uint32_t* out_rows); + /** @} */ #ifdef __cplusplus diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 9098f9dbc..dcdd1f1b8 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -241,6 +241,8 @@ comptime { @export(&c.kitty_graphics_placement_next, .{ .name = "ghostty_kitty_graphics_placement_next" }); @export(&c.kitty_graphics_placement_get, .{ .name = "ghostty_kitty_graphics_placement_get" }); @export(&c.kitty_graphics_placement_rect, .{ .name = "ghostty_kitty_graphics_placement_rect" }); + @export(&c.kitty_graphics_placement_pixel_size, .{ .name = "ghostty_kitty_graphics_placement_pixel_size" }); + @export(&c.kitty_graphics_placement_grid_size, .{ .name = "ghostty_kitty_graphics_placement_grid_size" }); @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); diff --git a/src/renderer/image.zig b/src/renderer/image.zig index c43d27981..442b7543f 100644 --- a/src/renderer/image.zig +++ b/src/renderer/image.zig @@ -426,7 +426,7 @@ pub const State = struct { // Calculate the dimensions of our image, taking in to // account the rows / columns specified by the placement. - const dest_size = p.calculatedSize(image.*, t); + const dest_size = p.pixelSize(image.*, t); // Calculate the source rectangle const source_x = @min(image.width, p.source_x); diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index 70ad5f818..9ed58e308 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -297,6 +297,48 @@ pub fn placement_rect( return .success; } +pub fn placement_pixel_size( + iter_: PlacementIterator, + image_: ImageHandle, + terminal_: terminal_c.Terminal, + out_width: *u32, + out_height: *u32, +) callconv(lib.calling_conv) Result { + if (comptime !build_options.kitty_graphics) return .no_value; + + const wrapper = terminal_ orelse return .invalid_value; + const image = image_ orelse return .invalid_value; + const iter = iter_ orelse return .invalid_value; + const entry = iter.entry orelse return .invalid_value; + const s = entry.value_ptr.pixelSize(image.*, wrapper.terminal); + + out_width.* = s.width; + out_height.* = s.height; + + return .success; +} + +pub fn placement_grid_size( + iter_: PlacementIterator, + image_: ImageHandle, + terminal_: terminal_c.Terminal, + out_cols: *u32, + out_rows: *u32, +) callconv(lib.calling_conv) Result { + if (comptime !build_options.kitty_graphics) return .no_value; + + const wrapper = terminal_ orelse return .invalid_value; + const image = image_ orelse return .invalid_value; + const iter = iter_ orelse return .invalid_value; + const entry = iter.entry orelse return .invalid_value; + const s = entry.value_ptr.gridSize(image.*, wrapper.terminal); + + out_cols.* = s.cols; + out_rows.* = s.rows; + + return .success; +} + test "placement_iterator new/free" { var iter: PlacementIterator = null; try testing.expectEqual(Result.success, placement_iterator_new( @@ -613,6 +655,115 @@ test "placement_rect null args return invalid_value" { try testing.expectEqual(Result.invalid_value, placement_rect(null, null, null, &sel)); } +test "placement_pixel_size with transmit and display" { + 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); + + // 80 cols * 10px = 800px, 24 rows * 20px = 480px. + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); + + // Transmit and display a 1x2 RGB image with c=10,r=1. + // 10 cols * 10px = 100px width, 1 row * 20px = 20px height. + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\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 iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(placement_iterator_next(iter)); + + var w: u32 = undefined; + var h: u32 = undefined; + try testing.expectEqual(Result.success, placement_pixel_size(iter, img, t, &w, &h)); + + try testing.expectEqual(100, w); + try testing.expectEqual(20, h); +} + +test "placement_pixel_size null args return invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var w: u32 = undefined; + var h: u32 = undefined; + try testing.expectEqual(Result.invalid_value, placement_pixel_size(null, null, null, &w, &h)); +} + +test "placement_grid_size with transmit and display" { + 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); + + // 80 cols * 10px = 800px, 24 rows * 20px = 480px. + try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); + + // Transmit and display a 1x2 RGB image with c=10,r=1. + const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\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 iter: PlacementIterator = null; + try testing.expectEqual(Result.success, placement_iterator_new( + &lib.alloc.test_allocator, + &iter, + )); + defer placement_iterator_free(iter); + + try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter))); + try testing.expect(placement_iterator_next(iter)); + + var cols: u32 = undefined; + var rows: u32 = undefined; + try testing.expectEqual(Result.success, placement_grid_size(iter, img, t, &cols, &rows)); + + try testing.expectEqual(10, cols); + try testing.expectEqual(1, rows); +} + +test "placement_grid_size null args return invalid_value" { + if (comptime !build_options.kitty_graphics) return error.SkipZigTest; + + var cols: u32 = undefined; + var rows: u32 = undefined; + try testing.expectEqual(Result.invalid_value, placement_grid_size(null, null, null, &cols, &rows)); +} + test "image_get on null returns invalid_value" { if (comptime !build_options.kitty_graphics) return error.SkipZigTest; diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 76c471dd0..2599bb971 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -17,6 +17,8 @@ pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iter pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next; pub const kitty_graphics_placement_get = kitty_graphics.placement_get; pub const kitty_graphics_placement_rect = kitty_graphics.placement_rect; +pub const kitty_graphics_placement_pixel_size = kitty_graphics.placement_pixel_size; +pub const kitty_graphics_placement_grid_size = kitty_graphics.placement_grid_size; pub const types = @import("types.zig"); pub const modes = @import("modes.zig"); pub const osc = @import("osc.zig"); diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 65c26dc85..e017d5f79 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -662,9 +662,10 @@ pub const ImageStorage = struct { } } - /// Calculates the size of this placement's image in pixels, - /// taking in to account the specified rows and columns. - pub fn calculatedSize( + /// Returns the size of this placement's image in pixels, + /// taking into account the source rectangle, specified + /// rows/columns, and aspect ratio. + pub fn pixelSize( self: Placement, image: Image, t: *const terminal.Terminal, @@ -759,7 +760,7 @@ pub const ImageStorage = struct { // Otherwise we calculate the pixel size, divide by // cell size, and round up to the nearest integer. - const calc_size = self.calculatedSize(image, t); + const calc_size = self.pixelSize(image, t); return .{ .cols = std.math.divCeil( u32, @@ -1338,7 +1339,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" { // that's 100px width. 100px * (9 / 16) = 56.25, which should round // to a height of 56px. - const calc_size = placement.calculatedSize(image, &t); + const calc_size = placement.pixelSize(image, &t); try testing.expectEqual(@as(u32, 100), calc_size.width); try testing.expectEqual(@as(u32, 56), calc_size.height); } @@ -1356,7 +1357,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" { // 100px height. 100px * (16 / 9) = 177.77..., which should round to // a width of 178px. - const calc_size = placement.calculatedSize(image, &t); + const calc_size = placement.pixelSize(image, &t); try testing.expectEqual(@as(u32, 178), calc_size.width); try testing.expectEqual(@as(u32, 100), calc_size.height); } From 426dc40799407b3ec564324438b73cce03f79835 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:07:09 -0700 Subject: [PATCH 07/11] example: update c-vt-kitty-graphics to use new APIs --- example/c-vt-kitty-graphics/src/main.c | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/example/c-vt-kitty-graphics/src/main.c b/example/c-vt-kitty-graphics/src/main.c index f3478811b..fb8087697 100644 --- a/example/c-vt-kitty-graphics/src/main.c +++ b/example/c-vt-kitty-graphics/src/main.c @@ -82,6 +82,9 @@ int main() { return 1; } + /* Set cell pixel dimensions so kitty graphics can compute grid sizes. */ + ghostty_terminal_resize(terminal, 80, 24, 8, 16); + /* Set a storage limit to enable Kitty graphics. */ uint64_t storage_limit = 64 * 1024 * 1024; /* 64 MiB */ ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, @@ -113,6 +116,83 @@ int main() { printf("PNG decode calls: %d\n", decode_count); + /* Query the kitty graphics storage to verify the image was stored. */ + GhosttyKittyGraphics graphics = NULL; + if (ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, + &graphics) != GHOSTTY_SUCCESS || !graphics) { + fprintf(stderr, "Failed to get kitty graphics storage\n"); + return 1; + } + printf("\nKitty graphics storage is available.\n"); + + /* Iterate placements to find the image ID. */ + GhosttyKittyGraphicsPlacementIterator iter = NULL; + if (ghostty_kitty_graphics_placement_iterator_new(NULL, &iter) != GHOSTTY_SUCCESS) { + fprintf(stderr, "Failed to create placement iterator\n"); + return 1; + } + if (ghostty_kitty_graphics_get(graphics, + GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &iter) != GHOSTTY_SUCCESS) { + fprintf(stderr, "Failed to get placement iterator\n"); + return 1; + } + + int placement_count = 0; + while (ghostty_kitty_graphics_placement_next(iter)) { + placement_count++; + uint32_t image_id = 0; + uint32_t placement_id = 0; + bool is_virtual = false; + int32_t z = 0; + + ghostty_kitty_graphics_placement_get(iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id); + ghostty_kitty_graphics_placement_get(iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID, &placement_id); + ghostty_kitty_graphics_placement_get(iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, &is_virtual); + ghostty_kitty_graphics_placement_get(iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, &z); + + printf(" placement #%d: image_id=%u placement_id=%u virtual=%s z=%d\n", + placement_count, image_id, placement_id, + is_virtual ? "true" : "false", z); + + /* Look up the image and print its properties. */ + GhosttyKittyGraphicsImage image = + ghostty_kitty_graphics_image(graphics, image_id); + if (!image) { + fprintf(stderr, "Failed to look up image %u\n", image_id); + return 1; + } + + uint32_t width = 0, height = 0, number = 0; + GhosttyKittyImageFormat format = 0; + size_t data_len = 0; + + ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_NUMBER, &number); + ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width); + ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height); + ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format); + ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len); + + printf(" image: number=%u size=%ux%u format=%d data_len=%zu\n", + number, width, height, format, data_len); + + /* Compute the rendered pixel size and grid size. */ + uint32_t px_w = 0, px_h = 0, cols = 0, rows = 0; + if (ghostty_kitty_graphics_placement_pixel_size(iter, image, terminal, + &px_w, &px_h) == GHOSTTY_SUCCESS) { + printf(" rendered pixel size: %ux%u\n", px_w, px_h); + } + if (ghostty_kitty_graphics_placement_grid_size(iter, image, terminal, + &cols, &rows) == GHOSTTY_SUCCESS) { + printf(" grid size: %u cols x %u rows\n", cols, rows); + } + } + printf("Total placements: %d\n", placement_count); + ghostty_kitty_graphics_placement_iterator_free(iter); + /* Clean up. */ ghostty_terminal_free(terminal); From 68a8cbb065028b8de4f3b9e0d1676891e8018bbe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:09:06 -0700 Subject: [PATCH 08/11] libghostty: fix expected format in image_get test The test transmits an image with f=24 (24-bit RGB) but was asserting that the format field equals .rgba (32-bit). Corrected the expectation to .rgb to match the transmitted pixel format. --- src/terminal/c/kitty_graphics.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index 9ed58e308..5444b8bab 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -583,7 +583,7 @@ test "image_get_handle and image_get with transmitted image" { var fmt: ImageFormat = undefined; try testing.expectEqual(Result.success, image_get(img, .format, @ptrCast(&fmt))); - try testing.expectEqual(.rgba, fmt); + try testing.expectEqual(.rgb, fmt); var comp: ImageCompression = undefined; try testing.expectEqual(Result.success, image_get(img, .compression, @ptrCast(&comp))); From fc9299a41df3c2cb7c987350e9a5cb67433e2835 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:14:24 -0700 Subject: [PATCH 09/11] libghostty: rename ghostty_kitty_image_get to ghostty_kitty_graphics_image_get Rename the public API function to follow the consistent ghostty_kitty_graphics_* naming convention used by the other kitty graphics API symbols. --- example/c-vt-kitty-graphics/src/main.c | 10 +++++----- include/ghostty/vt/kitty_graphics.h | 4 ++-- src/lib_vt.zig | 2 +- src/terminal/c/main.zig | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/c-vt-kitty-graphics/src/main.c b/example/c-vt-kitty-graphics/src/main.c index fb8087697..5001c3707 100644 --- a/example/c-vt-kitty-graphics/src/main.c +++ b/example/c-vt-kitty-graphics/src/main.c @@ -170,11 +170,11 @@ int main() { GhosttyKittyImageFormat format = 0; size_t data_len = 0; - ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_NUMBER, &number); - ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width); - ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height); - ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format); - ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_NUMBER, &number); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len); printf(" image: number=%u size=%ux%u format=%d data_len=%zu\n", number, width, height, format, data_len); diff --git a/include/ghostty/vt/kitty_graphics.h b/include/ghostty/vt/kitty_graphics.h index 124dc4265..e31bb2c65 100644 --- a/include/ghostty/vt/kitty_graphics.h +++ b/include/ghostty/vt/kitty_graphics.h @@ -162,7 +162,7 @@ typedef enum { } GhosttyKittyImageCompression; /** - * Queryable data kinds for ghostty_kitty_image_get(). + * Queryable data kinds for ghostty_kitty_graphics_image_get(). * * @ingroup kitty_graphics */ @@ -277,7 +277,7 @@ GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image( * * @ingroup kitty_graphics */ -GHOSTTY_API GhosttyResult ghostty_kitty_image_get( +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get( GhosttyKittyGraphicsImage image, GhosttyKittyGraphicsImageData data, void* out); diff --git a/src/lib_vt.zig b/src/lib_vt.zig index dcdd1f1b8..ce2e4d5b6 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -235,7 +235,7 @@ comptime { @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_image_get, .{ .name = "ghostty_kitty_graphics_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/main.zig b/src/terminal/c/main.zig index 2599bb971..ebfe2571d 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -11,7 +11,7 @@ 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_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; From 20b7fe0e1dd485af1f64ff5ce5d08135274896e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:30:00 -0700 Subject: [PATCH 10/11] libghostty: gate kitty graphics placement types on build option The PlacementIterator, PlacementMap, and PlacementIteratorWrapper types in the C API were unconditionally referencing kitty_storage.ImageStorage, which transitively pulled in Image.transmit_time (std.time.Instant). On wasm32-freestanding, std.time.Instant requires posix.timespec which does not exist, causing a compilation error. Gate these types behind build_options.kitty_graphics, matching the existing pattern used for KittyGraphics and ImageHandle. When kitty graphics is disabled, they fall back to opaque/void types. Add early-return guards to placement_iterator_new and placement_iterator_free which directly operate on the wrapper struct. --- src/terminal/c/kitty_graphics.zig | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index 5444b8bab..f5811a024 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -24,18 +24,27 @@ else ?*const anyopaque; /// C: GhosttyKittyGraphicsPlacementIterator -pub const PlacementIterator = ?*PlacementIteratorWrapper; +pub const PlacementIterator = if (build_options.kitty_graphics) + ?*PlacementIteratorWrapper +else + ?*anyopaque; -const PlacementMap = std.AutoHashMapUnmanaged( - kitty_storage.ImageStorage.PlacementKey, - kitty_storage.ImageStorage.Placement, -); +const PlacementMap = if (build_options.kitty_graphics) + std.AutoHashMapUnmanaged( + kitty_storage.ImageStorage.PlacementKey, + kitty_storage.ImageStorage.Placement, + ) +else + void; -const PlacementIteratorWrapper = struct { - alloc: std.mem.Allocator, - inner: PlacementMap.Iterator = undefined, - entry: ?PlacementMap.Entry = null, -}; +const PlacementIteratorWrapper = if (build_options.kitty_graphics) + struct { + alloc: std.mem.Allocator, + inner: PlacementMap.Iterator = undefined, + entry: ?PlacementMap.Entry = null, + } +else + void; /// C: GhosttyKittyGraphicsData pub const Data = enum(c_int) { @@ -204,6 +213,10 @@ pub fn placement_iterator_new( alloc_: ?*const CAllocator, out: *PlacementIterator, ) callconv(lib.calling_conv) Result { + if (comptime !build_options.kitty_graphics) { + out.* = null; + return .no_value; + } const alloc = lib.alloc.default(alloc_); const ptr = alloc.create(PlacementIteratorWrapper) catch { out.* = null; @@ -215,6 +228,7 @@ pub fn placement_iterator_new( } pub fn placement_iterator_free(iter_: PlacementIterator) callconv(lib.calling_conv) void { + if (comptime !build_options.kitty_graphics) return; const iter = iter_ orelse return; iter.alloc.destroy(iter); } From 6b94c2da26653cc8feeaee3ef90166b3ad1e3aee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:45:03 -0700 Subject: [PATCH 11/11] libghostty: add ghostty_terminal_point_from_grid_ref Add the inverse of ghostty_terminal_grid_ref(), converting a grid reference back to coordinates in a requested coordinate system (active, viewport, screen, or history). This wraps the existing internal PageList.pointFromPin and is placed on the terminal API since it requires terminal-owned PageList state to resolve the top-left anchor for each coordinate system. Returns GHOSTTY_NO_VALUE when the ref falls outside the requested range, e.g. a scrollback ref cannot be expressed in active coordinates. --- include/ghostty/vt/terminal.h | 33 ++++++++++ src/lib_vt.zig | 1 + src/terminal/c/main.zig | 1 + src/terminal/c/terminal.zig | 110 ++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 73db8d6d1..a229dd700 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -1064,6 +1064,39 @@ GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal, GhosttyPoint point, GhosttyGridRef *out_ref); +/** + * Convert a grid reference back to a point in the given coordinate system. + * + * This is the inverse of ghostty_terminal_grid_ref(): given a grid reference, + * it returns the x/y coordinates in the requested coordinate system (active, + * viewport, screen, or history). + * + * The grid reference must have been obtained from the same terminal instance. + * Like all grid references, it is only valid until the next mutating terminal + * call. + * + * Not every grid reference is representable in every coordinate system. For + * example, a cell in scrollback history cannot be expressed in active + * coordinates, and a cell that has scrolled off the visible area cannot be + * expressed in viewport coordinates. In these cases, the function returns + * GHOSTTY_NO_VALUE. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param ref Pointer to the grid reference to convert + * @param tag The target coordinate system + * @param[out] out On success, set to the coordinate in the requested system (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * or ref is NULL/invalid, GHOSTTY_NO_VALUE if the ref falls outside + * the requested coordinate system + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_point_from_grid_ref( + GhosttyTerminal terminal, + const GhosttyGridRef *ref, + GhosttyPointTag tag, + GhosttyPointCoordinate *out); + /** @} */ #ifdef __cplusplus diff --git a/src/lib_vt.zig b/src/lib_vt.zig index ce2e4d5b6..3799fbe66 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -233,6 +233,7 @@ comptime { @export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" }); @export(&c.terminal_get, .{ .name = "ghostty_terminal_get" }); @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); + @export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_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_graphics_image_get, .{ .name = "ghostty_kitty_graphics_image_get" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index ebfe2571d..3f5f65f49 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -157,6 +157,7 @@ pub const terminal_mode_get = terminal.mode_get; pub const terminal_mode_set = terminal.mode_set; pub const terminal_get = terminal.get; pub const terminal_grid_ref = terminal.grid_ref; +pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref; pub const type_json = types.get_json; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 32bc0311a..8a2a3d40b 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -697,6 +697,20 @@ pub fn grid_ref( return .success; } +pub fn point_from_grid_ref( + terminal_: Terminal, + ref: *const grid_ref_c.CGridRef, + tag: point.Tag, + out: ?*point.Coordinate, +) callconv(lib.calling_conv) Result { + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + const p = ref.toPin() orelse return .invalid_value; + const pt = t.screens.active.pages.pointFromPin(tag, p) orelse + return .no_value; + if (out) |o| o.* = pt.coord(); + return .success; +} + pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void { const wrapper = terminal_ orelse return; const t = wrapper.terminal; @@ -1261,6 +1275,102 @@ test "grid_ref null terminal" { }, &out_ref)); } +test "point_from_grid_ref roundtrip active" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + + // Get a grid ref at (2, 0) in active coords + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .active, + .value = .{ .active = .{ .x = 2, .y = 0 } }, + }, &ref)); + + // Convert back to active coords + var coord: point.Coordinate = undefined; + try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .active, &coord)); + try testing.expectEqual(@as(size.CellCountInt, 2), coord.x); + try testing.expectEqual(@as(u32, 0), coord.y); +} + +test "point_from_grid_ref roundtrip viewport" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .viewport, + .value = .{ .viewport = .{ .x = 0, .y = 0 } }, + }, &ref)); + + var coord: point.Coordinate = undefined; + try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .viewport, &coord)); + try testing.expectEqual(@as(size.CellCountInt, 0), coord.x); + try testing.expectEqual(@as(u32, 0), coord.y); +} + +test "point_from_grid_ref history ref to active returns no_value" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 4, .max_scrollback = 10_000 }, + )); + defer free(t); + + // Write enough lines to push content into scrollback + for (0..10) |_| { + vt_write(t, "line\n", 5); + } + + // Get a ref to the first line (now in scrollback) + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .screen, + .value = .{ .screen = .{ .x = 0, .y = 0 } }, + }, &ref)); + + // Should succeed for screen coords + var coord: point.Coordinate = undefined; + try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .screen, &coord)); + try testing.expectEqual(@as(u32, 0), coord.y); + + // Should fail for active coords (it's in scrollback) + try testing.expectEqual(Result.no_value, point_from_grid_ref(t, &ref, .active, &coord)); +} + +test "point_from_grid_ref null terminal" { + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.invalid_value, point_from_grid_ref(null, &ref, .active, null)); +} + +test "point_from_grid_ref null node" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer free(t); + + const ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null)); +} + test "set write_pty callback" { var t: Terminal = null; try testing.expectEqual(Result.success, new(