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.
pull/12144/head
Mitchell Hashimoto 2026-04-06 07:39:31 -07:00
parent 3a52e0e3bd
commit d7fa92088c
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
5 changed files with 268 additions and 0 deletions

View File

@ -118,6 +118,7 @@ extern "C" {
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/style.h>
#include <ghostty/vt/sys.h>
#include <ghostty/vt/key.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/mouse.h>

125
include/ghostty/vt/sys.h Normal file
View File

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
/** @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 */

View File

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

View File

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

137
src/terminal/c/sys.zig Normal file
View File

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