Fix zig build test-lib-vt (#11778)

- Our `checkGhosttyH` calls need to be guarded on building Ghostty app
which has it
- Move FileFormatter to its own file to avoid poisoning test refs with
Config.zig which pulls in the world
- Move WindowPaddingBalance to renderer to avoid pulling in Config.zig
- Add a `zig build test-lib-vt` CI job
This commit is contained in:
Mitchell Hashimoto
2026-03-23 09:38:40 -07:00
committed by GitHub
17 changed files with 175 additions and 119 deletions

View File

@@ -105,6 +105,7 @@ jobs:
- test-sentry-linux
- test-i18n
- test-fuzz-libghostty
- test-lib-vt
- test-macos
- pinact
- prettier
@@ -911,6 +912,36 @@ jobs:
- name: Test System Build
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
test-lib-vt:
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip
runs-on: namespace-profile-ghostty-md
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Test
run: nix develop -c zig build test-lib-vt
test-gtk:
strategy:
fail-fast: false

View File

@@ -2,6 +2,7 @@ const builtin = @import("builtin");
const file_load = @import("config/file_load.zig");
const formatter = @import("config/formatter.zig");
const formatter_file = @import("config/formatter_file.zig");
pub const Config = @import("config/Config.zig");
pub const conditional = @import("config/conditional.zig");
pub const io = @import("config/io.zig");
@@ -10,7 +11,7 @@ pub const edit = @import("config/edit.zig");
pub const url = @import("config/url.zig");
pub const ConditionalState = conditional.State;
pub const FileFormatter = formatter.FileFormatter;
pub const FileFormatter = formatter_file.FileFormatter;
pub const entryFormatter = formatter.entryFormatter;
pub const formatEntry = formatter.formatEntry;
pub const preferredDefaultFilePath = file_load.preferredDefaultFilePath;

View File

@@ -39,6 +39,7 @@ pub const Path = @import("path.zig").Path;
pub const RepeatablePath = @import("path.zig").RepeatablePath;
const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig");
const KeyRemapSet = @import("../input/key_mods.zig").RemapSet;
pub const WindowPaddingBalance = @import("../renderer/size.zig").PaddingBalance;
const string = @import("string.zig");
// We do this instead of importing all of terminal/main.zig to
@@ -5245,12 +5246,6 @@ pub const Fullscreen = enum(c_int) {
@"non-native-padded-notch",
};
pub const WindowPaddingBalance = enum {
false,
true,
equal,
};
pub const WindowPaddingColor = enum {
background,
extend,

View File

@@ -1,8 +1,7 @@
const formatter = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const help_strings = @import("help_strings");
const Config = @import("Config.zig");
const Key = @import("key.zig").Key;
/// Returns a single entry formatter for the given field name and writer.
@@ -125,106 +124,6 @@ pub fn formatEntry(
@compileError("missing case for type");
}
/// FileFormatter is a formatter implementation that outputs the
/// config in a file-like format. This uses more generous whitespace,
/// can include comments, etc.
pub const FileFormatter = struct {
alloc: Allocator,
config: *const Config,
/// Include comments for documentation of each key
docs: bool = false,
/// Only include changed values from the default.
changed: bool = false,
/// Implements std.fmt so it can be used directly with std.fmt.
pub fn format(
self: FileFormatter,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
@setEvalBranchQuota(10_000);
// If we're change-tracking then we need the default config to
// compare against.
var default: ?Config = if (self.changed)
Config.default(self.alloc) catch return error.WriteFailed
else
null;
defer if (default) |*v| v.deinit();
inline for (@typeInfo(Config).@"struct".fields) |field| {
if (field.name[0] == '_') continue;
const value = @field(self.config, field.name);
const do_format = if (default) |d| format: {
const key = @field(Key, field.name);
break :format d.changed(self.config, key);
} else true;
if (do_format) {
const do_docs = self.docs and @hasDecl(help_strings.Config, field.name);
if (do_docs) {
const help = @field(help_strings.Config, field.name);
var lines = std.mem.splitScalar(u8, help, '\n');
while (lines.next()) |line| {
try writer.print("# {s}\n", .{line});
}
}
formatEntry(
field.type,
field.name,
value,
writer,
) catch return error.WriteFailed;
if (do_docs) try writer.print("\n", .{});
}
}
}
};
test "format default config" {
const testing = std.testing;
const alloc = testing.allocator;
var cfg = try Config.default(alloc);
defer cfg.deinit();
var buf: std.Io.Writer.Allocating = .init(alloc);
defer buf.deinit();
// We just make sure this works without errors. We aren't asserting output.
const fmt: FileFormatter = .{
.alloc = alloc,
.config = &cfg,
};
try fmt.format(&buf.writer);
//std.log.warn("{s}", .{buf.written()});
}
test "format default config changed" {
const testing = std.testing;
const alloc = testing.allocator;
var cfg = try Config.default(alloc);
defer cfg.deinit();
cfg.@"font-size" = 42;
var buf: std.Io.Writer.Allocating = .init(alloc);
defer buf.deinit();
// We just make sure this works without errors. We aren't asserting output.
const fmt: FileFormatter = .{
.alloc = alloc,
.config = &cfg,
.changed = true,
};
try fmt.format(&buf.writer);
//std.log.warn("{s}", .{buf.written()});
}
test "formatEntry bool" {
const testing = std.testing;

View File

@@ -0,0 +1,110 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Config = @import("Config.zig");
const Key = @import("key.zig").Key;
const help_strings = @import("help_strings");
const formatter = @import("formatter.zig");
// IMPORTANT: This is in a separate file from formatter.zig because it
// puts a build-time dependency on Config.zig which brings in too much
// into libghostty-vt tests which reference some formattable types.
/// FileFormatter is a formatter implementation that outputs the
/// config in a file-like format. This uses more generous whitespace,
/// can include comments, etc.
pub const FileFormatter = struct {
alloc: Allocator,
config: *const Config,
/// Include comments for documentation of each key
docs: bool = false,
/// Only include changed values from the default.
changed: bool = false,
/// Implements std.fmt so it can be used directly with std.fmt.
pub fn format(
self: FileFormatter,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
@setEvalBranchQuota(10_000);
// If we're change-tracking then we need the default config to
// compare against.
var default: ?Config = if (self.changed)
Config.default(self.alloc) catch return error.WriteFailed
else
null;
defer if (default) |*v| v.deinit();
inline for (@typeInfo(Config).@"struct".fields) |field| {
if (field.name[0] == '_') continue;
const value = @field(self.config, field.name);
const do_format = if (default) |d| format: {
const key = @field(Key, field.name);
break :format d.changed(self.config, key);
} else true;
if (do_format) {
const do_docs = self.docs and @hasDecl(help_strings.Config, field.name);
if (do_docs) {
const help = @field(help_strings.Config, field.name);
var lines = std.mem.splitScalar(u8, help, '\n');
while (lines.next()) |line| {
try writer.print("# {s}\n", .{line});
}
}
formatter.formatEntry(
field.type,
field.name,
value,
writer,
) catch return error.WriteFailed;
if (do_docs) try writer.print("\n", .{});
}
}
}
};
test "format default config" {
const testing = std.testing;
const alloc = testing.allocator;
var cfg = try Config.default(alloc);
defer cfg.deinit();
var buf: std.Io.Writer.Allocating = .init(alloc);
defer buf.deinit();
// We just make sure this works without errors. We aren't asserting output.
const fmt: FileFormatter = .{
.alloc = alloc,
.config = &cfg,
};
try fmt.format(&buf.writer);
//std.log.warn("{s}", .{buf.written()});
}
test "format default config changed" {
const testing = std.testing;
const alloc = testing.allocator;
var cfg = try Config.default(alloc);
defer cfg.deinit();
cfg.@"font-size" = 42;
var buf: std.Io.Writer.Allocating = .init(alloc);
defer buf.deinit();
// We just make sure this works without errors. We aren't asserting output.
const fmt: FileFormatter = .{
.alloc = alloc,
.config = &cfg,
.changed = true,
};
try fmt.format(&buf.writer);
//std.log.warn("{s}", .{buf.written()});
}

View File

@@ -2,7 +2,6 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const cimgui = @import("dcimgui");
const OptionAsAlt = @import("config.zig").OptionAsAlt;
pub const Mods = @import("key_mods.zig").Mods;

View File

@@ -95,7 +95,10 @@ test "abi by removing a key" {
/// Verify that for every key in enum T, there is a matching declaration in
/// `ghostty.h` with the correct value. This should only ever be called inside a `test`
/// because the `ghostty.h` module is only available then.
pub fn checkGhosttyHEnum(comptime T: type, comptime prefix: []const u8) !void {
pub fn checkGhosttyHEnum(
comptime T: type,
comptime prefix: []const u8,
) !void {
const info = @typeInfo(T);
try std.testing.expect(info == .@"enum");

View File

@@ -1,10 +1,22 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const configpkg = @import("../config.zig");
const terminal_size = @import("../terminal/size.zig");
const log = std.log.scoped(.renderer_size);
/// Controls how extra whitespace around the terminal grid is distributed.
pub const PaddingBalance = enum {
/// No balancing; padding is applied as specified explicitly.
false,
/// Balances padding but caps the top padding so the first row doesn't
/// drift too far from the top of the window. Excess vertical space is
/// shifted to the bottom.
true,
/// Distributes leftover space equally on all sides so the grid is
/// centered within the screen.
equal,
};
/// All relevant sizes for a rendered terminal. These are all the sizes that
/// any functionality should need to know about the terminal in order to
/// convert between any coordinate systems.
@@ -37,7 +49,7 @@ pub const Size = struct {
pub fn balancePadding(
self: *Size,
explicit: Padding,
mode: configpkg.Config.WindowPaddingBalance,
mode: PaddingBalance,
) void {
// This ensure grid() does the right thing
self.padding = explicit;

View File

@@ -43,6 +43,7 @@ pub fn get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| getTyped(
comptime_data,
@ptrCast(@alignCast(out)),

View File

@@ -110,6 +110,7 @@ pub fn get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| getTyped(
cell_,
comptime_data,

View File

@@ -259,7 +259,7 @@ test "setopt_from_terminal" {
// Options should reflect defaults from a fresh terminal
try testing.expect(!e.?.opts.cursor_key_application);
try testing.expect(!e.?.opts.alt_esc_prefix);
try testing.expect(e.?.opts.alt_esc_prefix);
try testing.expectEqual(KittyFlags.disabled, e.?.opts.kitty_flags);
try testing.expectEqual(OptionAsAlt.false, e.?.opts.macos_option_as_alt);
}

View File

@@ -76,6 +76,7 @@ pub fn commandData(
}
return switch (data) {
.invalid => false,
inline else => |comptime_data| commandDataTyped(
command_,
comptime_data,

View File

@@ -193,6 +193,7 @@ pub fn get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| getTyped(
state_,
comptime_data,
@@ -336,11 +337,7 @@ pub fn colors_get(
out_colors.cursor_has_value = colors.cursor != null;
}
if (lib.structSizedFieldFits(
Colors,
out_size,
"palette",
)) {
{
const palette_offset = @offsetOf(Colors, "palette");
if (out_size > palette_offset) {
const available = out_size - palette_offset;
@@ -467,6 +464,7 @@ pub fn row_cells_get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| rowCellsGetTyped(
cells_,
comptime_data,
@@ -566,6 +564,7 @@ pub fn row_get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| rowGetTyped(
iterator_,
comptime_data,

View File

@@ -73,6 +73,7 @@ pub fn get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| getTyped(
row_,
comptime_data,

View File

@@ -196,6 +196,7 @@ pub fn get(
}
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| getTyped(
terminal_,
comptime_data,

View File

@@ -92,6 +92,7 @@ pub const Shape = enum(c_int) {
};
test "ghostty.h MouseShape" {
if (comptime build_options.artifact == .lib) return error.SkipZigTest;
try lib.checkGhosttyHEnum(Shape, "GHOSTTY_MOUSE_SHAPE_");
}
};

View File

@@ -205,6 +205,7 @@ pub const Command = union(Key) {
pause,
test "ghostty.h Command.ProgressReport.State" {
if (comptime build_options.artifact == .lib) return error.SkipZigTest;
try lib.checkGhosttyHEnum(State, "GHOSTTY_PROGRESS_STATE_");
}
};