core: add tests for ghostty.h

* ensure that `ghostty.h` compiles during basic Zig tests
* ensure that non-exhaustive enums are kept synchronized between
  `ghostty.h` and their respective Zig counterpart.
* adjust some enums that varied from established conventions
This commit is contained in:
Jeffrey C. Ollie
2026-02-27 00:52:47 -06:00
parent b30db91e69
commit ea5b07d20f
11 changed files with 172 additions and 11 deletions

View File

@@ -292,6 +292,7 @@ pub fn build(b: *std.Build) !void {
// Crash on x86_64 without this
.use_llvm = true,
});
test_exe.root_module.addIncludePath(b.path("include"));
if (config.emit_test_exe) b.installArtifact(test_exe);
_ = try deps.add(test_exe);

View File

@@ -586,9 +586,9 @@ typedef enum {
// apprt.action.Fullscreen
typedef enum {
GHOSTTY_FULLSCREEN_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE,
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU,
GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH,
} ghostty_action_fullscreen_e;
// apprt.action.FloatWindow
@@ -718,7 +718,7 @@ typedef struct {
// renderer.Health
typedef enum {
GHOSTTY_RENDERER_HEALTH_OK,
GHOSTTY_RENDERER_HEALTH_HEALTHY,
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
} ghostty_action_renderer_health_e;

View File

@@ -7,13 +7,13 @@ extension FullscreenMode {
case GHOSTTY_FULLSCREEN_NATIVE:
.native
case GHOSTTY_FULLSCREEN_NON_NATIVE:
case GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE:
.nonNative
case GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU:
case GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU:
.nonNativeVisibleMenu
case GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH:
case GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH:
.nonNativePaddedNotch
default:

View File

@@ -678,7 +678,7 @@ extension Ghostty {
guard let healthAny = notification.userInfo?["health"] else { return }
guard let health = healthAny as? ghostty_action_renderer_health_e else { return }
DispatchQueue.main.async { [weak self] in
self?.healthy = health == GHOSTTY_RENDERER_HEALTH_OK
self?.healthy = health == GHOSTTY_RENDERER_HEALTH_HEALTHY
}
}

View File

@@ -7,6 +7,7 @@ const input = @import("../input.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const CoreSurface = @import("../Surface.zig");
const lib = @import("../lib/main.zig");
/// The target for an action. This is generally the thing that had focus
/// while the action was made but the concept of "focus" is not guaranteed
@@ -19,6 +20,10 @@ pub const Target = union(Key) {
pub const Key = enum(c_int) {
app,
surface,
test "ghostty_h Target.Key" {
try lib.checkGhosttyHEnum(Key, "GHOSTTY_TARGET_");
}
};
// Sync with: ghostty_target_u
@@ -401,6 +406,10 @@ pub const Action = union(Key) {
search_selected,
readonly,
copy_title_to_clipboard,
test "ghostty_h Action.Key" {
try lib.checkGhosttyHEnum(Key, "GHOSTTY_ACTION_");
}
};
/// Sync with: ghostty_action_u
@@ -482,6 +491,10 @@ pub const SplitDirection = enum(c_int) {
down,
left,
up,
test "ghostty_h SplitDirection" {
try lib.checkGhosttyHEnum(SplitDirection, "GHOSTTY_SPLIT_DIRECTION_");
}
};
// This is made extern (c_int) to make interop easier with our embedded
@@ -494,6 +507,10 @@ pub const GotoSplit = enum(c_int) {
left,
down,
right,
test "ghostty_h GotoSplit" {
try lib.checkGhosttyHEnum(GotoSplit, "GHOSTTY_GOTO_SPLIT_");
}
};
// This is made extern (c_int) to make interop easier with our embedded
@@ -501,6 +518,10 @@ pub const GotoSplit = enum(c_int) {
pub const GotoWindow = enum(c_int) {
previous,
next,
test "ghostty_h GotoWindow" {
try lib.checkGhosttyHEnum(GotoWindow, "GHOSTTY_GOTO_WINDOW_");
}
};
/// The amount to resize the split by and the direction to resize it in.
@@ -513,6 +534,10 @@ pub const ResizeSplit = extern struct {
down,
left,
right,
test "ghostty_h ResizeSplit.Direction" {
try lib.checkGhosttyHEnum(Direction, "GHOSTTY_RESIZE_SPLIT_");
}
};
};
@@ -528,6 +553,11 @@ pub const GotoTab = enum(c_int) {
next = -2,
last = -3,
_,
// TODO: check non-exhaustive enums
// test "ghostty_h GotoTab" {
// try lib.checkGhosttyHEnum(GotoTab, "GHOSTTY_GOTO_TAB_");
// }
};
/// The fullscreen mode to toggle to if we're moving to fullscreen.
@@ -539,18 +569,30 @@ pub const Fullscreen = enum(c_int) {
macos_non_native,
macos_non_native_visible_menu,
macos_non_native_padded_notch,
test "ghostty_h Fullscreen" {
try lib.checkGhosttyHEnum(Fullscreen, "GHOSTTY_FULLSCREEN_");
}
};
pub const FloatWindow = enum(c_int) {
on,
off,
toggle,
test "ghostty_h FloatWindow" {
try lib.checkGhosttyHEnum(FloatWindow, "GHOSTTY_FLOAT_WINDOW_");
}
};
pub const SecureInput = enum(c_int) {
on,
off,
toggle,
test "ghostty_h SecureInput" {
try lib.checkGhosttyHEnum(SecureInput, "GHOSTTY_SECURE_INPUT_");
}
};
/// The inspector mode to toggle to if we're toggling the inspector.
@@ -558,27 +600,47 @@ pub const Inspector = enum(c_int) {
toggle,
show,
hide,
test "ghostty_h Inspector" {
try lib.checkGhosttyHEnum(Inspector, "GHOSTTY_INSPECTOR_");
}
};
pub const QuitTimer = enum(c_int) {
start,
stop,
test "ghostty_h QuitTimer" {
try lib.checkGhosttyHEnum(QuitTimer, "GHOSTTY_QUIT_TIMER_");
}
};
pub const Readonly = enum(c_int) {
off,
on,
test "ghostty_h Readonly" {
try lib.checkGhosttyHEnum(Readonly, "GHOSTTY_READONLY_");
}
};
pub const MouseVisibility = enum(c_int) {
visible,
hidden,
test "ghostty_h MouseVisibility" {
try lib.checkGhosttyHEnum(MouseVisibility, "GHOSTTY_MOUSE_");
}
};
/// Whether to prompt for the surface title or tab title.
pub const PromptTitle = enum(c_int) {
surface,
tab,
test "ghostty_h PromptTitle" {
try lib.checkGhosttyHEnum(PromptTitle, "GHOSTTY_PROMPT_TITLE_");
}
};
pub const MouseOverLink = struct {
@@ -782,6 +844,11 @@ pub const ColorKind = enum(c_int) {
// 0+ values indicate a palette index
_,
// TODO: check non-non-exhaustive enums
// test "ghostty_h ColorKind" {
// try lib.checkGhosttyHEnum(ColorKind, "GHOSTTY_COLOR_KIND_");
// }
};
pub const ReloadConfig = extern struct {
@@ -832,6 +899,10 @@ pub const OpenUrl = struct {
/// The URL is known to contain HTML content.
html,
test "ghostty_h OpenUrl.Kind" {
try lib.checkGhosttyHEnum(Kind, "GHOSTTY_ACTION_OPEN_URL_KIND_");
}
};
// Sync with: ghostty_action_open_url_s
@@ -858,6 +929,10 @@ pub const CloseTabMode = enum(c_int) {
other,
/// Close all tabs to the right of the current tab.
right,
test "ghostty_h CloseTabMode" {
try lib.checkGhosttyHEnum(CloseTabMode, "GHOSTTY_ACTION_CLOSE_TAB_MODE_");
}
};
pub const CommandFinished = struct {
@@ -922,3 +997,7 @@ pub const SearchSelected = struct {
};
}
};
test {
_ = std.testing.refAllDeclsRecursive(@This());
}

View File

@@ -3,6 +3,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = @import("../quirks.zig").inlineAssert;
const lib = @import("../lib/main.zig");
pub const Errors = error{
/// The IPC failed. If a function returns this error, it's expected that
@@ -22,6 +23,10 @@ pub const Target = union(Key) {
pub const Key = enum(c_int) {
class,
detect,
test "ghostty_h Target.Key" {
try lib.checkGhosttyHEnum(Key, "GHOSTTY_IPC_TARGET_");
}
};
// Sync with: ghostty_ipc_target_u
@@ -106,8 +111,12 @@ pub const Action = union(enum) {
};
/// Sync with: ghostty_ipc_action_tag_e
pub const Key = enum(c_uint) {
pub const Key = enum(c_int) {
new_window,
test "ghostty_h Action.Key" {
try lib.checkGhosttyHEnum(Key, "GHOSTTY_IPC_ACTION_");
}
};
/// Sync with: ghostty_ipc_action_u

View File

@@ -91,3 +91,55 @@ test "abi by removing a key" {
try testing.expectEqual(2, @intFromEnum(T.d));
}
}
/// Verify that for every key in enum T, there is a matching declaration in
/// `ghostty.h` with the correct value.
pub fn checkGhosttyHEnum(comptime T: type, comptime prefix: []const u8) !void {
const info = @typeInfo(T);
try std.testing.expect(info == .@"enum");
try std.testing.expect(info.@"enum".tag_type == c_int);
@setEvalBranchQuota(1000000);
const c = @cImport({
@cInclude("ghostty.h");
});
var set: std.EnumSet(T) = .initFull();
const c_decls = @typeInfo(c).@"struct".decls;
const enum_fields = info.@"enum".fields;
inline for (enum_fields) |field| {
const upper_name = comptime u: {
var buf: [128]u8 = undefined;
break :u std.ascii.upperString(&buf, field.name);
};
inline for (c_decls) |decl| {
if (!comptime std.mem.startsWith(u8, decl.name, prefix)) continue;
const suffix = decl.name[prefix.len..];
if (!comptime std.mem.eql(u8, suffix, upper_name)) continue;
std.testing.expectEqual(field.value, @field(c, decl.name)) catch |e| {
std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ decl.name, .{});
return e;
};
set.remove(@enumFromInt(field.value));
}
}
std.testing.expect(set.count() == 0) catch |e| {
var it = set.iterator();
while (it.next()) |v| {
var buf: [128]u8 = undefined;
const n = std.ascii.upperString(&buf, @tagName(v));
std.log.err("ghostty.h is missing value for {s}{s}, {t}", .{ prefix, n, v });
}
return e;
};
}

View File

@@ -5,6 +5,7 @@ const unionpkg = @import("union.zig");
pub const allocator = @import("allocator.zig");
pub const Enum = enumpkg.Enum;
pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum;
pub const String = types.String;
pub const Struct = @import("struct.zig").Struct;
pub const Target = @import("target.zig").Target;

View File

@@ -31,6 +31,7 @@ pub const ScreenSize = size.ScreenSize;
pub const GridSize = size.GridSize;
pub const Padding = size.Padding;
pub const cursorStyle = cursor.style;
pub const lib = @import("lib/main.zig");
/// The implementation to use for the renderer. This is comptime chosen
/// so that every build has exactly one renderer implementation.
@@ -44,8 +45,12 @@ pub const Renderer = switch (build_config.renderer) {
/// renderers even if some states aren't reachable so that our API users
/// can use the same enum for all renderers.
pub const Health = enum(c_int) {
healthy = 0,
unhealthy = 1,
healthy,
unhealthy,
test "ghostty_h Health" {
try lib.checkGhosttyHEnum(Health, "GHOSTTY_RENDERER_HEALTH_");
}
};
test {

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const build_options = @import("terminal_options");
const lib = @import("../lib/main.zig");
/// The possible cursor shapes. Not all app runtimes support these shapes.
/// The shapes are always based on the W3C supported cursor styles so we
@@ -63,6 +64,10 @@ pub const MouseShape = enum(c_int) {
.none => void,
};
};
test "ghostty_h MouseShape" {
try lib.checkGhosttyHEnum(MouseShape, "GHOSTTY_MOUSE_SHAPE_");
}
};
const string_map = std.StaticStringMap(MouseShape).initComptime(.{
@@ -131,3 +136,7 @@ test "cursor shape from string" {
const testing = std.testing;
try testing.expectEqual(MouseShape.default, MouseShape.fromString("default").?);
}
test {
_ = std.testing.refAllDeclsRecursive(@This());
}

View File

@@ -15,6 +15,7 @@ const LibEnum = @import("../lib/enum.zig").Enum;
const kitty_color = @import("kitty/color.zig");
const parsers = @import("osc/parsers.zig");
const encoding = @import("osc/encoding.zig");
const lib = @import("../lib/main.zig");
pub const color = parsers.color;
pub const semantic_prompt = parsers.semantic_prompt;
@@ -197,6 +198,10 @@ pub const Command = union(Key) {
@"error",
indeterminate,
pause,
test "ghostty_h Command.ProgressReport.State" {
try lib.checkGhosttyHEnum(State, "GHOSTTY_PROGRESS_STATE_");
}
};
state: State,