terminal: make wuffs runtime-swappable, enable Kitty graphics for libvt

Introduce terminal/sys.zig which provides runtime-swappable function
pointers for operations that depend on external implementations. This
allows embedders of the terminal package to swap out implementations
at startup without hard dependencies on specific libraries.

The first function exposed is decode_png, which defaults to a wuffs
implementation. The kitty graphics image loader now calls through
sys.decode_png instead of importing wuffs directly.

This allows us to enable Kitty graphics support in libghostty-vt
for all targets except wasm32-freestanding.
This commit is contained in:
Mitchell Hashimoto
2026-04-04 21:36:54 -07:00
parent c541ceb120
commit 29e3de737e
5 changed files with 96 additions and 5 deletions

View File

@@ -19,6 +19,24 @@ const builtin = @import("builtin");
// or are too Ghostty-internal.
const terminal = @import("terminal/main.zig");
/// System interface for the terminal package.
///
/// This module provides runtime-swappable function pointers for operations
/// that depend on external implementations. Embedders can use this to
/// provide or override default behaviors. These must be set at startup
/// before any terminal functionality is used.
///
/// This lets libghostty-vt have no runtime dependencies on external
/// libraries, while still allowing rich functionality that may require
/// external libraries (e.g. image decoding or regular expresssions).
///
/// Setting these will enable various features of the terminal package.
/// For example, setting a PNG decoder will enable support for PNG images in
/// the Kitty Graphics Protocol.
///
/// Additional functionality will be added here over time as needed.
pub const sys = terminal.sys;
pub const apc = terminal.apc;
pub const dcs = terminal.dcs;
pub const osc = terminal.osc;

View File

@@ -47,8 +47,23 @@ pub const Options = struct {
opts.addOption(bool, "simd", self.simd);
opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety);
// Kitty graphics is almost always true. This used to be conditional on
// some other factors but we've since generalized the implementation
// to support optional PNG decoding, OS capabilities like filesystems,
// etc. So its safe to always enable it and just have the
// implementation deal with unsupported features as needed.
//
// We disable it on wasm32-freestanding because we at the least
// require the ability to get timestamps and there is no way to
// do that with freestanding targets.
const target = m.resolved_target.?.result;
opts.addOption(
bool,
"kitty_graphics",
!(target.cpu.arch == .wasm32 and target.os.tag == .freestanding),
);
// These are synthesized based on other options.
opts.addOption(bool, "kitty_graphics", self.oniguruma);
opts.addOption(bool, "tmux_control_mode", self.oniguruma);
// Version information.

View File

@@ -8,7 +8,7 @@ const posix = std.posix;
const fastmem = @import("../../fastmem.zig");
const command = @import("graphics_command.zig");
const PageList = @import("../PageList.zig");
const wuffs = @import("wuffs");
const sys = @import("../sys.zig");
const temp_dir = struct {
const TempDir = @import("../../os/TempDir.zig");
@@ -426,13 +426,14 @@ pub const LoadingImage = struct {
fn decodePng(self: *LoadingImage, alloc: Allocator) !void {
assert(self.image.format == .png);
const result = wuffs.png.decode(
const decode_png_fn = sys.decode_png orelse
return error.UnsupportedFormat;
const result = decode_png_fn(
alloc,
self.data.items,
) catch |err| switch (err) {
error.WuffsError => return error.InvalidData,
error.InvalidData => return error.InvalidData,
error.OutOfMemory => return error.OutOfMemory,
error.Overflow => return error.InvalidData,
};
defer alloc.free(result.data);
@@ -799,6 +800,8 @@ test "image load: rgb, not compressed, regular file" {
}
test "image load: png, not compressed, regular file" {
if (sys.decode_png == null) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@@ -23,6 +23,7 @@ pub const search = @import("search.zig");
pub const sgr = @import("sgr.zig");
pub const size = @import("size.zig");
pub const size_report = @import("size_report.zig");
pub const sys = @import("sys.zig");
pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {};
pub const x11_color = @import("x11_color.zig");

54
src/terminal/sys.zig Normal file
View File

@@ -0,0 +1,54 @@
//! System interface for the terminal package.
//!
//! This provides runtime-swappable function pointers for operations that
//! depend on external implementations (e.g. image decoding). Each function
//! pointer is initialized with a default implementation if available.
//!
//! This exists so that the terminal package doesn't have hard dependencies
//! on specific libraries and enables embedders of the terminal package to
//! swap out implementations as needed at startup to provide their own
//! implementations.
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_options = @import("terminal_options");
/// Decode PNG data into RGBA pixels. If null, PNG decoding is unsupported
/// and the exact semantics are up to callers. For example, the Kitty Graphics
/// Protocol will work but cannot accept PNG images.
pub var decode_png: ?DecodePngFn = png: {
if (build_options.artifact == .lib) break :png null;
break :png &decodePngWuffs;
};
pub const DecodeError = Allocator.Error || error{InvalidData};
pub const DecodePngFn = *const fn (Allocator, []const u8) DecodeError!Image;
/// The result of decoding an image. The caller owns the returned data
/// and must free it with the same allocator that was passed to the
/// decode function.
pub const Image = struct {
width: u32,
height: u32,
data: []u8,
};
fn decodePngWuffs(
alloc: Allocator,
data: []const u8,
) DecodeError!Image {
const wuffs = @import("wuffs");
const result = wuffs.png.decode(
alloc,
data,
) catch |err| switch (err) {
error.WuffsError => return error.InvalidData,
error.OutOfMemory => return error.OutOfMemory,
error.Overflow => return error.InvalidData,
};
return .{
.width = result.width,
.height = result.height,
.data = result.data,
};
}