From 29e3de737e9cc4c4d6a3ac9624bbd26c87bf0eb2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Apr 2026 21:36:54 -0700 Subject: [PATCH] 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. --- src/lib_vt.zig | 18 +++++++++ src/terminal/build_options.zig | 17 ++++++++- src/terminal/kitty/graphics_image.zig | 11 ++++-- src/terminal/main.zig | 1 + src/terminal/sys.zig | 54 +++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/terminal/sys.zig diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 3edef835a..665058b68 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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; diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 6c0a4df63..5f851c55c 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -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. diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index d2877cfc2..bf11507b4 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -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; diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 9f5b65e34..87a9aded9 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -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"); diff --git a/src/terminal/sys.zig b/src/terminal/sys.zig new file mode 100644 index 000000000..f0c64da50 --- /dev/null +++ b/src/terminal/sys.zig @@ -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, + }; +}