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
parent
3a52e0e3bd
commit
d7fa92088c
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue