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 + * * @{ */