From 3a52e0e3bdba98b5372cf0f2d5ca5f150b8c09d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 07:13:48 -0700 Subject: [PATCH 1/3] libghostty: expose kitty image options via terminal set/get Add four new terminal options for configuring Kitty graphics at runtime through the C API: storage limit, and the three loading medium flags (file, temporary file, shared memory). The storage limit setter propagates to all initialized screens and uses setLimit which handles eviction when lowering the limit. The medium setters similarly propagate to all screens. Getters read from the active screen. All options compile to no-ops or return no_value when kitty graphics are disabled at build time. --- include/ghostty/vt/terminal.h | 83 +++++++++++++++++++++++++++++++++++ src/terminal/c/terminal.zig | 61 +++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index b43e37cf4..c243fa25c 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -534,6 +534,49 @@ typedef enum { * Input type: GhosttyColorRgb[256]* */ GHOSTTY_TERMINAL_OPT_COLOR_PALETTE = 14, + + /** + * Set the Kitty image storage limit in bytes. + * + * Applied to all initialized screens (primary and alternate). + * A value of zero disables the Kitty graphics protocol entirely, + * deleting all stored images and placements. A NULL value pointer + * is equivalent to zero (disables). Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: uint64_t* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT = 15, + + /** + * Enable or disable Kitty image loading via the file medium. + * + * A NULL value pointer is a no-op. Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16, + + /** + * Enable or disable Kitty image loading via the temporary file medium. + * + * A NULL value pointer is a no-op. Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17, + + /** + * Enable or disable Kitty image loading via the shared memory medium. + * + * A NULL value pointer is a no-op. Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM = 18, } GhosttyTerminalOption; /** @@ -756,6 +799,46 @@ typedef enum { * Output type: GhosttyColorRgb[256] * */ GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT = 25, + + /** + * The Kitty image storage limit in bytes for the active screen. + * + * A value of zero means the Kitty graphics protocol is disabled. + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: uint64_t * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_STORAGE_LIMIT = 26, + + /** + * Whether the file medium is enabled for Kitty image loading on the + * active screen. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE = 27, + + /** + * Whether the temporary file medium is enabled for Kitty image loading + * on the active screen. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE = 28, + + /** + * Whether the shared memory medium is enabled for Kitty image loading + * on the active screen. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM = 29, } GhosttyTerminalData; /** diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index ec749ffae..a2b0d1092 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -1,5 +1,6 @@ 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 ZigTerminal = @import("../Terminal.zig"); @@ -304,6 +305,10 @@ pub const Option = enum(c_int) { color_background = 12, color_cursor = 13, color_palette = 14, + kitty_image_storage_limit = 15, + kitty_image_medium_file = 16, + kitty_image_medium_temp_file = 17, + kitty_image_medium_shared_mem = 18, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -320,6 +325,11 @@ pub const Option = enum(c_int) { .title, .pwd => ?*const lib.String, .color_foreground, .color_background, .color_cursor => ?*const color.RGB.C, .color_palette => ?*const color.PaletteC, + .kitty_image_storage_limit => ?*const u64, + .kitty_image_medium_file, + .kitty_image_medium_temp_file, + .kitty_image_medium_shared_mem, + => ?*const bool, }; } }; @@ -388,6 +398,32 @@ fn setTyped( ); wrapper.terminal.flags.dirty.palette = true; }, + .kitty_image_storage_limit => { + if (comptime !build_options.kitty_graphics) return .success; + const limit: usize = if (value) |v| @intCast(v.*) else 0; + var it = wrapper.terminal.screens.all.iterator(); + while (it.next()) |entry| { + const screen = entry.value.*; + screen.kitty_images.setLimit(screen.alloc, screen, limit) catch return .out_of_memory; + } + }, + .kitty_image_medium_file, + .kitty_image_medium_temp_file, + .kitty_image_medium_shared_mem, + => { + if (comptime !build_options.kitty_graphics) return .success; + const val = (value orelse return .success).*; + var it = wrapper.terminal.screens.all.iterator(); + while (it.next()) |entry| { + const screen = entry.value.*; + switch (option) { + .kitty_image_medium_file => screen.kitty_images.image_limits.file = val, + .kitty_image_medium_temp_file => screen.kitty_images.image_limits.temporary_file = val, + .kitty_image_medium_shared_mem => screen.kitty_images.image_limits.shared_memory = val, + else => unreachable, + } + } + }, } return .success; } @@ -513,6 +549,10 @@ pub const TerminalData = enum(c_int) { color_background_default = 23, color_cursor_default = 24, color_palette_default = 25, + kitty_image_storage_limit = 26, + kitty_image_medium_file = 27, + kitty_image_medium_temp_file = 28, + kitty_image_medium_shared_mem = 29, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { @@ -535,6 +575,11 @@ pub const TerminalData = enum(c_int) { .color_cursor_default, => color.RGB.C, .color_palette, .color_palette_default => color.PaletteC, + .kitty_image_storage_limit => u64, + .kitty_image_medium_file, + .kitty_image_medium_temp_file, + .kitty_image_medium_shared_mem, + => bool, }; } }; @@ -603,6 +648,22 @@ fn getTyped( .color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(), .color_palette => out.* = color.paletteCval(&t.colors.palette.current), .color_palette_default => out.* = color.paletteCval(&t.colors.palette.original), + .kitty_image_storage_limit => { + if (comptime !build_options.kitty_graphics) return .no_value; + out.* = @intCast(t.screens.active.kitty_images.total_limit); + }, + .kitty_image_medium_file => { + if (comptime !build_options.kitty_graphics) return .no_value; + out.* = t.screens.active.kitty_images.image_limits.file; + }, + .kitty_image_medium_temp_file => { + if (comptime !build_options.kitty_graphics) return .no_value; + out.* = t.screens.active.kitty_images.image_limits.temporary_file; + }, + .kitty_image_medium_shared_mem => { + if (comptime !build_options.kitty_graphics) return .no_value; + out.* = t.screens.active.kitty_images.image_limits.shared_memory; + }, } return .success; From d7fa92088c0e50d02d97190973b91d49d0c39d6a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 07:39:31 -0700 Subject: [PATCH 2/3] libghostty: expose sys interface to C API The terminal sys module provides runtime-swappable function pointers for operations that depend on external implementations (e.g. PNG decoding). This exposes that functionality through the C API via a ghostty_sys_set() function, modeled after the ghostty_terminal_set() enum-based option pattern. Embedders can install a PNG decode callback to enable Kitty Graphics Protocol PNG support. The callback receives a userdata pointer (set via GHOSTTY_SYS_OPT_USERDATA) and a GhosttyAllocator that must be used to allocate the returned pixel data, since the library takes ownership of the buffer. Passing NULL clears the callback and disables the feature. --- include/ghostty/vt.h | 1 + include/ghostty/vt/sys.h | 125 +++++++++++++++++++++++++++++++++++ src/lib_vt.zig | 1 + src/terminal/c/main.zig | 4 ++ src/terminal/c/sys.zig | 137 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+) create mode 100644 include/ghostty/vt/sys.h create mode 100644 src/terminal/c/sys.zig diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 6a943350c..0d54e2d2f 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -118,6 +118,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/include/ghostty/vt/sys.h b/include/ghostty/vt/sys.h new file mode 100644 index 000000000..7c9a366bb --- /dev/null +++ b/include/ghostty/vt/sys.h @@ -0,0 +1,125 @@ +/** + * @file sys.h + * + * System interface - runtime-swappable implementations for external dependencies. + */ + +#ifndef GHOSTTY_VT_SYS_H +#define GHOSTTY_VT_SYS_H + +#include +#include +#include +#include +#include + +/** @defgroup sys System Interface + * + * Runtime-swappable function pointers for operations that depend on + * external implementations (e.g. image decoding). + * + * These are process-global settings that must be configured at startup + * before any terminal functionality that depends on them is used. + * Setting these enables various optional features of the terminal. For + * example, setting a PNG decoder enables PNG image support in the Kitty + * Graphics Protocol. + * + * Use ghostty_sys_set() with a `GhosttySysOption` to install or clear + * an implementation. Passing NULL as the value clears the implementation + * and disables the corresponding feature. + * + * @{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Result of decoding an image. + * + * The `data` buffer must be allocated through the allocator provided to + * the decode callback. The library takes ownership and will free it + * with the same allocator. + */ +typedef struct { + /** Image width in pixels. */ + uint32_t width; + + /** Image height in pixels. */ + uint32_t height; + + /** Pointer to the decoded RGBA pixel data. */ + uint8_t* data; + + /** Length of the pixel data in bytes. */ + size_t data_len; +} GhosttySysImage; + +/** + * Callback type for PNG decoding. + * + * Decodes raw PNG data into RGBA pixels. The output pixel data must be + * allocated through the provided allocator. The library takes ownership + * of the buffer and will free it with the same allocator. + * + * @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA + * @param allocator The allocator to use for the output pixel buffer + * @param data Pointer to the raw PNG data + * @param data_len Length of the raw PNG data in bytes + * @param[out] out On success, filled with the decoded image + * @return true on success, false on failure + */ +typedef bool (*GhosttySysDecodePngFn)( + void* userdata, + const GhosttyAllocator* allocator, + const uint8_t* data, + size_t data_len, + GhosttySysImage* out); + +/** + * System option identifiers for ghostty_sys_set(). + */ +typedef enum { + /** + * Set the userdata pointer passed to all sys callbacks. + * + * Input type: void* (or NULL) + */ + GHOSTTY_SYS_OPT_USERDATA = 0, + + /** + * Set the PNG decode function. + * + * When set, the terminal can accept PNG images via the Kitty + * Graphics Protocol. When cleared (NULL value), PNG decoding is + * unsupported and PNG image data will be rejected. + * + * Input type: GhosttySysDecodePngFn (function pointer, or NULL) + */ + GHOSTTY_SYS_OPT_DECODE_PNG = 1, +} GhosttySysOption; + +/** + * Set a system-level option. + * + * Configures a process-global implementation function. These should be + * set once at startup before using any terminal functionality that + * depends on them. + * + * @param option The option to set + * @param value Pointer to the value (type depends on the option), + * or NULL to clear it + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * option is not recognized + */ +GHOSTTY_API GhosttyResult ghostty_sys_set(GhosttySysOption option, + const void* value); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SYS_H */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 665058b68..deee9633c 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -189,6 +189,7 @@ comptime { @export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" }); @export(&c.style_default, .{ .name = "ghostty_style_default" }); @export(&c.style_is_default, .{ .name = "ghostty_style_is_default" }); + @export(&c.sys_set, .{ .name = "ghostty_sys_set" }); @export(&c.cell_get, .{ .name = "ghostty_cell_get" }); @export(&c.row_get, .{ .name = "ghostty_row_get" }); @export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index dc3b7e7ce..997a8e2c8 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -22,6 +22,7 @@ pub const row = @import("row.zig"); pub const sgr = @import("sgr.zig"); pub const size_report = @import("size_report.zig"); pub const style = @import("style.zig"); +pub const sys = @import("sys.zig"); pub const terminal = @import("terminal.zig"); // The full C API, unexported. @@ -132,6 +133,8 @@ pub const row_get = row.get; pub const style_default = style.default_style; pub const style_is_default = style.style_is_default; +pub const sys_set = sys.set; + pub const terminal_new = terminal.new; pub const terminal_free = terminal.free; pub const terminal_reset = terminal.reset; @@ -173,6 +176,7 @@ test { _ = sgr; _ = size_report; _ = style; + _ = sys; _ = terminal; _ = types; diff --git a/src/terminal/c/sys.zig b/src/terminal/c/sys.zig new file mode 100644 index 000000000..9677c8794 --- /dev/null +++ b/src/terminal/c/sys.zig @@ -0,0 +1,137 @@ +const std = @import("std"); +const lib = @import("../lib.zig"); +const CAllocator = lib.alloc.Allocator; +const terminal_sys = @import("../sys.zig"); +const Result = @import("result.zig").Result; + +/// C: GhosttySysImage +pub const Image = extern struct { + width: u32, + height: u32, + data: ?[*]u8, + data_len: usize, +}; + +/// C: GhosttySysDecodePngFn +pub const DecodePngFn = *const fn ( + ?*anyopaque, + *const CAllocator, + [*]const u8, + usize, + *Image, +) callconv(lib.calling_conv) bool; + +/// C: GhosttySysOption +pub const Option = enum(c_int) { + userdata = 0, + decode_png = 1, + + pub fn InType(comptime self: Option) type { + return switch (self) { + .userdata => ?*const anyopaque, + .decode_png => ?DecodePngFn, + }; + } +}; + +/// Global state for the sys interface so we can call through to the C +/// callbacks from Zig. +const Global = struct { + userdata: ?*anyopaque = null, + decode_png: ?DecodePngFn = null, +}; + +/// Global state for the C sys interface. +var global: Global = .{}; + +/// Zig-compatible wrapper that calls through to the stored C callback. +/// The C callback allocates the pixel data through the provided allocator, +/// so we can take ownership directly. +fn decodePngWrapper( + alloc: std.mem.Allocator, + data: []const u8, +) terminal_sys.DecodeError!terminal_sys.Image { + const func = global.decode_png orelse return error.InvalidData; + + const c_alloc = CAllocator.fromZig(&alloc); + var out: Image = undefined; + if (!func(global.userdata, &c_alloc, data.ptr, data.len, &out)) return error.InvalidData; + + const result_data = out.data orelse return error.InvalidData; + + return .{ + .width = out.width, + .height = out.height, + .data = result_data[0..out.data_len], + }; +} + +pub fn set( + option: Option, + value: ?*const anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch { + return .invalid_value; + }; + } + + return switch (option) { + inline else => |comptime_option| setTyped( + comptime_option, + @ptrCast(@alignCast(value)), + ), + }; +} + +fn setTyped( + comptime option: Option, + value: option.InType(), +) Result { + switch (option) { + .userdata => global.userdata = @constCast(value), + .decode_png => { + global.decode_png = value; + terminal_sys.decode_png = if (value != null) &decodePngWrapper else null; + }, + } + return .success; +} + +test "set decode_png with null clears" { + // Start from a known state. + global.decode_png = null; + terminal_sys.decode_png = null; + + try std.testing.expectEqual(Result.success, set(.decode_png, null)); + try std.testing.expect(terminal_sys.decode_png == null); +} + +test "set decode_png installs wrapper" { + const S = struct { + fn decode(_: ?*anyopaque, _: *const CAllocator, _: [*]const u8, _: usize, out: *Image) callconv(lib.calling_conv) bool { + out.* = .{ .width = 1, .height = 1, .data = null, .data_len = 0 }; + return true; + } + }; + + try std.testing.expectEqual(Result.success, set( + .decode_png, + @ptrCast(&S.decode), + )); + try std.testing.expect(terminal_sys.decode_png != null); + + // Clear it again. + try std.testing.expectEqual(Result.success, set(.decode_png, null)); + try std.testing.expect(terminal_sys.decode_png == null); +} + +test "set userdata" { + var data: u32 = 42; + try std.testing.expectEqual(Result.success, set(.userdata, @ptrCast(&data))); + try std.testing.expect(global.userdata == @as(?*anyopaque, @ptrCast(&data))); + + // Clear it. + try std.testing.expectEqual(Result.success, set(.userdata, null)); + try std.testing.expect(global.userdata == null); +} From 64340c62bfab76147d6fa4aec4d4979d3c4d2e33 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 07:49:49 -0700 Subject: [PATCH 3/3] example: add c-vt-kitty-graphics Demonstrates the sys interface for Kitty Graphics Protocol PNG support. The example installs a PNG decode callback via ghostty_sys_set, creates a terminal with image storage enabled, and sends an inline 1x1 PNG image through vt_write. Snippet markers are wired up to the sys.h doxygen docs. --- example/c-vt-kitty-graphics/README.md | 18 ++++ example/c-vt-kitty-graphics/build.zig | 42 ++++++++ example/c-vt-kitty-graphics/build.zig.zon | 24 +++++ example/c-vt-kitty-graphics/src/main.c | 125 ++++++++++++++++++++++ include/ghostty/vt.h | 5 + include/ghostty/vt/sys.h | 8 ++ 6 files changed, 222 insertions(+) create mode 100644 example/c-vt-kitty-graphics/README.md create mode 100644 example/c-vt-kitty-graphics/build.zig create mode 100644 example/c-vt-kitty-graphics/build.zig.zon create mode 100644 example/c-vt-kitty-graphics/src/main.c diff --git a/example/c-vt-kitty-graphics/README.md b/example/c-vt-kitty-graphics/README.md new file mode 100644 index 000000000..cbeb67476 --- /dev/null +++ b/example/c-vt-kitty-graphics/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Kitty Graphics Protocol + +This contains a simple example of how to use the system interface +(`ghostty_sys_set`) to install a PNG decoder callback, then send +a Kitty Graphics Protocol image via `ghostty_terminal_vt_write`. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-kitty-graphics/build.zig b/example/c-vt-kitty-graphics/build.zig new file mode 100644 index 000000000..4bbf9e3ff --- /dev/null +++ b/example/c-vt-kitty-graphics/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_kitty_graphics", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-kitty-graphics/build.zig.zon b/example/c-vt-kitty-graphics/build.zig.zon new file mode 100644 index 000000000..fce0e5906 --- /dev/null +++ b/example/c-vt-kitty-graphics/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_kitty_graphics, + .version = "0.0.0", + .fingerprint = 0x432d40ecc8f15589, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-kitty-graphics/src/main.c b/example/c-vt-kitty-graphics/src/main.c new file mode 100644 index 000000000..f3478811b --- /dev/null +++ b/example/c-vt-kitty-graphics/src/main.c @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include + +//! [kitty-graphics-decode-png] +/** + * Minimal PNG decoder callback for the sys interface. + * + * A real implementation would use a PNG library (libpng, stb_image, etc.) + * to decode the PNG data. This example uses a hardcoded 1x1 red pixel + * since we know exactly what image we're sending. + * + * WARNING: This is only an example for providing a callback, it DOES NOT + * actually decode the PNG it is passed. It hardcodes a response. + */ +bool decode_png(void* userdata, + const GhosttyAllocator* allocator, + const uint8_t* data, + size_t data_len, + GhosttySysImage* out) { + int* count = (int*)userdata; + (*count)++; + printf(" decode_png called (size=%zu, call #%d)\n", data_len, *count); + + /* Allocate RGBA pixel data through the provided allocator. */ + const size_t pixel_len = 4; /* 1x1 RGBA */ + uint8_t* pixels = ghostty_alloc(allocator, pixel_len); + if (!pixels) return false; + + /* Fill with red (R=255, G=0, B=0, A=255). */ + pixels[0] = 255; + pixels[1] = 0; + pixels[2] = 0; + pixels[3] = 255; + + out->width = 1; + out->height = 1; + out->data = pixels; + out->data_len = pixel_len; + return true; +} +//! [kitty-graphics-decode-png] + +//! [kitty-graphics-write-pty] +/** + * write_pty callback to capture terminal responses. + * + * The Kitty graphics protocol sends an APC response back to the pty + * when an image is loaded (unless suppressed with q=2). + */ +void on_write_pty(GhosttyTerminal terminal, + void* userdata, + const uint8_t* data, + size_t len) { + (void)terminal; + (void)userdata; + printf(" response (%zu bytes): ", len); + fwrite(data, 1, len, stdout); + printf("\n"); +} +//! [kitty-graphics-write-pty] + +//! [kitty-graphics-main] +int main() { + /* Install the PNG decoder via the sys interface. */ + int decode_count = 0; + ghostty_sys_set(GHOSTTY_SYS_OPT_USERDATA, &decode_count); + ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void*)decode_png); + + /* Create a terminal with Kitty graphics enabled. */ + GhosttyTerminal terminal = NULL; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) { + fprintf(stderr, "Failed to create terminal\n"); + return 1; + } + + /* 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, + &storage_limit); + + /* Install write_pty to see the protocol response. */ + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, + (const void*)on_write_pty); + + /* + * Send a Kitty graphics command with an inline 1x1 PNG image. + * + * The escape sequence is: + * ESC _G a=T,f=100,q=1; ESC \ + * + * Where: + * a=T — transmit and display + * f=100 — PNG format + * q=1 — request a response (q=0 would suppress it) + */ + printf("Sending Kitty graphics PNG image:\n"); + const char* kitty_cmd = + "\x1b_Ga=T,f=100,q=1;" + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" + "DUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" + "\x1b\\"; + ghostty_terminal_vt_write(terminal, (const uint8_t*)kitty_cmd, + strlen(kitty_cmd)); + + printf("PNG decode calls: %d\n", decode_count); + + /* Clean up. */ + ghostty_terminal_free(terminal); + + /* Clear the sys callbacks. */ + ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, NULL); + ghostty_sys_set(GHOSTTY_SYS_OPT_USERDATA, NULL); + + return 0; +} +//! [kitty-graphics-main] diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 0d54e2d2f..5dd06521c 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -98,6 +98,11 @@ * grid refs to inspect cell codepoints, row wrap state, and cell styles. */ +/** @example c-vt-kitty-graphics/src/main.c + * This example demonstrates how to use the system interface to install a + * PNG decoder callback and send a Kitty Graphics Protocol image. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H diff --git a/include/ghostty/vt/sys.h b/include/ghostty/vt/sys.h index 7c9a366bb..0634f5ac8 100644 --- a/include/ghostty/vt/sys.h +++ b/include/ghostty/vt/sys.h @@ -28,6 +28,14 @@ * an implementation. Passing NULL as the value clears the implementation * and disables the corresponding feature. * + * ## Example + * + * ### Defining a PNG decode callback + * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png + * + * ### Installing the callback and sending a PNG image + * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main + * * @{ */