Skip to content

Commit 771ad61

Browse files
committed
libghostty: add a test to ensure terminal modes are defined
Adds a test to make sure that all terminal modes defined in Zig have a corresponding definition in `ghostty/vt.h`.
1 parent 7421b4b commit 771ad61

File tree

6 files changed

+493
-14
lines changed

6 files changed

+493
-14
lines changed

build.zig

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const assert = std.debug.assert;
33
const builtin = @import("builtin");
44
const buildpkg = @import("src/build/main.zig");
55

6+
const c_deps = @import("src/build/c_deps.zig");
7+
68
/// App version from build.zig.zon.
79
const app_zon_version = @import("build.zig.zon").version;
810

@@ -318,13 +320,21 @@ pub fn build(b: *std.Build) !void {
318320
.root_module = mod.vt,
319321
.filters = test_filters,
320322
});
323+
try c_deps.add(b, .ghostty_vt_h, mod.vt, &config, .{
324+
.target = config.baselineTarget(),
325+
.optimize = .Debug,
326+
});
321327
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
322328
test_lib_vt_step.dependOn(&mod_vt_test_run.step);
323329

324330
const mod_vt_c_test = b.addTest(.{
325331
.root_module = mod.vt_c,
326332
.filters = test_filters,
327333
});
334+
try c_deps.add(b, .ghostty_vt_h, mod.vt_c, &config, .{
335+
.target = config.baselineTarget(),
336+
.optimize = .Debug,
337+
});
328338
const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test);
329339
test_lib_vt_step.dependOn(&mod_vt_c_test_run.step);
330340
}
@@ -349,13 +359,14 @@ pub fn build(b: *std.Build) !void {
349359
if (config.emit_test_exe) b.installArtifact(test_exe);
350360
_ = try deps.add(test_exe);
351361

352-
// Verify our internal libghostty header.
353-
const ghostty_h = b.addTranslateC(.{
354-
.root_source_file = b.path("include/ghostty.h"),
362+
try c_deps.add(b, .ghostty_h, test_exe.root_module, &config, .{
363+
.target = config.baselineTarget(),
364+
.optimize = .Debug,
365+
});
366+
try c_deps.add(b, .ghostty_vt_h, test_exe.root_module, &config, .{
355367
.target = config.baselineTarget(),
356368
.optimize = .Debug,
357369
});
358-
test_exe.root_module.addImport("ghostty.h", ghostty_h.createModule());
359370

360371
// Normal test running
361372
const test_run = b.addRunArtifact(test_exe);

include/ghostty/vt/modes.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ extern "C" {
4747
* @{
4848
*/
4949
#define GHOSTTY_MODE_KAM (ghostty_mode_new(2, true)) /**< Keyboard action (disable keyboard) */
50+
#define GHOSTTY_MODE_DISABLE_KEYBOARD GHOSTTY_MODE_KAM
5051
#define GHOSTTY_MODE_INSERT (ghostty_mode_new(4, true)) /**< Insert mode */
5152
#define GHOSTTY_MODE_SRM (ghostty_mode_new(12, true)) /**< Send/receive mode */
53+
#define GHOSTTY_MODE_SEND_RECEIVE_MODE GHOSTTY_MODE_SRM
5254
#define GHOSTTY_MODE_LINEFEED (ghostty_mode_new(20, true)) /**< Linefeed/new line mode */
5355
/** @} */
5456

@@ -57,41 +59,59 @@ extern "C" {
5759
* @{
5860
*/
5961
#define GHOSTTY_MODE_DECCKM (ghostty_mode_new(1, false)) /**< Cursor keys */
62+
#define GHOSTTY_MODE_CURSOR_KEYS GHOSTTY_MODE_DECCCKM
6063
#define GHOSTTY_MODE_132_COLUMN (ghostty_mode_new(3, false)) /**< 132/80 column mode */
6164
#define GHOSTTY_MODE_SLOW_SCROLL (ghostty_mode_new(4, false)) /**< Slow scroll */
6265
#define GHOSTTY_MODE_REVERSE_COLORS (ghostty_mode_new(5, false)) /**< Reverse video */
6366
#define GHOSTTY_MODE_ORIGIN (ghostty_mode_new(6, false)) /**< Origin mode */
6467
#define GHOSTTY_MODE_WRAPAROUND (ghostty_mode_new(7, false)) /**< Auto-wrap mode */
6568
#define GHOSTTY_MODE_AUTOREPEAT (ghostty_mode_new(8, false)) /**< Auto-repeat keys */
6669
#define GHOSTTY_MODE_X10_MOUSE (ghostty_mode_new(9, false)) /**< X10 mouse reporting */
70+
#define GHOSTTY_MODE_MOUSE_EVENT_X10 GHOSTTY_MODE_X10_MOUSE
6771
#define GHOSTTY_MODE_CURSOR_BLINKING (ghostty_mode_new(12, false)) /**< Cursor blink */
6872
#define GHOSTTY_MODE_CURSOR_VISIBLE (ghostty_mode_new(25, false)) /**< Cursor visible (DECTCEM) */
6973
#define GHOSTTY_MODE_ENABLE_MODE_3 (ghostty_mode_new(40, false)) /**< Allow 132 column mode */
7074
#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_new(45, false)) /**< Reverse wrap */
7175
#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_new(47, false)) /**< Alternate screen (legacy) */
7276
#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_new(66, false)) /**< Application keypad */
7377
#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_new(69, false)) /**< Left/right margin mode */
78+
#define GHOSTTY_MODE_ENABLE_LEFT_AND_RIGHT_MARGIN GHOSTTY_MODE_LEFT_RIGHT_MARGIN
7479
#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_new(1000, false)) /**< Normal mouse tracking */
80+
#define GHOSTTY_MODE_MOUSE_EVENT_NORMAL GHOSTTY_MODE_NORMAL_MOUSE
7581
#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_new(1002, false)) /**< Button-event mouse tracking */
82+
#define GHOSTTY_MODE_MOUSE_EVENT_BUTTON GHOSTTY_MODE_BUTTON_MOUSE
7683
#define GHOSTTY_MODE_ANY_MOUSE (ghostty_mode_new(1003, false)) /**< Any-event mouse tracking */
84+
#define GHOSTTY_MODE_MOUSE_EVENT_ANY GHOSTTY_MODE_ANY_MOUSE
7785
#define GHOSTTY_MODE_FOCUS_EVENT (ghostty_mode_new(1004, false)) /**< Focus in/out events */
7886
#define GHOSTTY_MODE_UTF8_MOUSE (ghostty_mode_new(1005, false)) /**< UTF-8 mouse format */
87+
#define GHOSTTY_MODE_MOUSE_FORMAT_UTF8 GHOSTTY_MODE_UTF8_MOUSE
7988
#define GHOSTTY_MODE_SGR_MOUSE (ghostty_mode_new(1006, false)) /**< SGR mouse format */
89+
#define GHOSTTY_MODE_MOUSE_FORMAT_SGR GHOSTTY_MODE_SGR_MOUSE
8090
#define GHOSTTY_MODE_ALT_SCROLL (ghostty_mode_new(1007, false)) /**< Alternate scroll mode */
91+
#define GHOSTTY_MODE_MOUSE_ALTERNATE_SCROLL GHOSTTY_MODE_ALT_SCROLL
8192
#define GHOSTTY_MODE_URXVT_MOUSE (ghostty_mode_new(1015, false)) /**< URxvt mouse format */
93+
#define GHOSTTY_MODE_MOUSE_FORMAT_URXVT GHOSTTY_MODE_URXVT_MOUSE
8294
#define GHOSTTY_MODE_SGR_PIXELS_MOUSE (ghostty_mode_new(1016, false)) /**< SGR-Pixels mouse format */
95+
#define GHOSTTY_MODE_MOUSE_FORMAT_SGR_PIXELS GHOSTTY_MODE_SGR_PIXELS_MOUSE
8396
#define GHOSTTY_MODE_NUMLOCK_KEYPAD (ghostty_mode_new(1035, false)) /**< Ignore keypad with NumLock */
97+
#define GHOSTTY_MODE_IGNORE_KEYPAD_WITH_NUMLOCK GHOSTTY_MODE_NUMLOCK_KEYPAD
8498
#define GHOSTTY_MODE_ALT_ESC_PREFIX (ghostty_mode_new(1036, false)) /**< Alt key sends ESC prefix */
8599
#define GHOSTTY_MODE_ALT_SENDS_ESC (ghostty_mode_new(1039, false)) /**< Alt sends escape */
100+
#define GHOSTTY_MODE_ALT_SENDS_ESCAPE GHOSTTY_MODE_ALT_SENDS_ESC
86101
#define GHOSTTY_MODE_REVERSE_WRAP_EXT (ghostty_mode_new(1045, false)) /**< Extended reverse wrap */
102+
#define GHOSTTY_MODE_REVERSE_WRAP_EXTENDED GHOSTTY_MODE_REVERSE_WRAP_EXT
87103
#define GHOSTTY_MODE_ALT_SCREEN (ghostty_mode_new(1047, false)) /**< Alternate screen */
88104
#define GHOSTTY_MODE_SAVE_CURSOR (ghostty_mode_new(1048, false)) /**< Save cursor (DECSC) */
89105
#define GHOSTTY_MODE_ALT_SCREEN_SAVE (ghostty_mode_new(1049, false)) /**< Alt screen + save cursor + clear */
106+
#define GHOSTTY_MODE_ALT_SCREEN_SAVE_CURSOR_CLEAR_ENTER GHOSTTY_ALT_SCREEN_SAVE
90107
#define GHOSTTY_MODE_BRACKETED_PASTE (ghostty_mode_new(2004, false)) /**< Bracketed paste mode */
91108
#define GHOSTTY_MODE_SYNC_OUTPUT (ghostty_mode_new(2026, false)) /**< Synchronized output */
109+
#define GHOSTTY_MODE_SYNCHRONIZED_OUTPUT GHOSTTY_MODE_SYNC_OUTPUT
92110
#define GHOSTTY_MODE_GRAPHEME_CLUSTER (ghostty_mode_new(2027, false)) /**< Grapheme cluster mode */
93111
#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_new(2031, false)) /**< Report color scheme */
112+
#define GHOSTTY_MODE_REPORT_COLOR_SCHEME GHOSTTY_MODE_COLOR_SCHEME_REPORT
94113
#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_new(2048, false)) /**< In-band size reports */
114+
#define GHOSTTY_MODE_IN_BAND_SIZE_REPORTS GHOSTTY_MODE_IN_BAND_RESIZE
95115
/** @} */
96116

97117
/**

src/build/c_deps.zig

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
const std = @import("std");
2+
3+
const Config = @import("Config.zig");
4+
5+
pub const CDeps = enum {
6+
ghostty_h,
7+
ghostty_vt_h,
8+
};
9+
10+
const Key = struct {
11+
dep: CDeps,
12+
target: std.Build.ResolvedTarget,
13+
optimize: std.builtin.OptimizeMode,
14+
};
15+
16+
const Value = *std.Build.Module;
17+
18+
const Context = struct {
19+
pub fn hash(_: Context, key: Key) u32 {
20+
var h: std.hash.XxHash32 = .init(0);
21+
h.update(std.mem.asBytes(&key.dep));
22+
h.update(std.mem.asBytes(&key.target.result.cpu.arch));
23+
h.update(key.target.result.cpu.features.asBytes());
24+
h.update(std.mem.asBytes(&key.target.result.os.tag));
25+
h.update(std.mem.asBytes(&key.target.result.abi));
26+
h.update(std.mem.asBytes(&key.optimize));
27+
return h.final();
28+
}
29+
30+
pub fn eql(_: Context, a: Key, b: Key, _: usize) bool {
31+
return a.dep == b.dep and
32+
a.target.result.cpu.arch == b.target.result.cpu.arch and
33+
a.target.result.cpu.features.eql(b.target.result.cpu.features) and
34+
a.target.result.os.tag == b.target.result.os.tag and
35+
a.target.result.abi == b.target.result.abi and
36+
a.optimize == b.optimize;
37+
}
38+
};
39+
40+
var modules: std.ArrayHashMapUnmanaged(Key, Value, Context, false) = .empty;
41+
42+
pub const Options = struct {
43+
target: ?std.Build.ResolvedTarget = null,
44+
optimize: ?std.builtin.OptimizeMode = null,
45+
};
46+
47+
/// Add the specified C dependency to the given module.
48+
pub fn add(b: *std.Build, dep: CDeps, module: *std.Build.Module, config: *const Config, options: Options) !void {
49+
const target = options.target orelse config.target;
50+
const optimize = options.optimize orelse config.optimize;
51+
52+
// TranslateC requires libc, and these targets don't have libc
53+
if (target.result.cpu.arch.isWasm()) return;
54+
if (target.result.os.tag == .windows) return;
55+
56+
const key: Key = .{
57+
.dep = dep,
58+
.target = target,
59+
.optimize = optimize,
60+
};
61+
62+
const value = modules.get(key) orelse value: {
63+
const value = switch (dep) {
64+
.ghostty_h => v: {
65+
// Verify our internal libghostty header.
66+
const ghostty_h = b.addTranslateC(.{
67+
.root_source_file = b.path("include/ghostty.h"),
68+
.target = target,
69+
.optimize = optimize,
70+
});
71+
break :v ghostty_h.createModule();
72+
},
73+
74+
.ghostty_vt_h => v: {
75+
// Verify our libghostty-vt header.
76+
const ghostty_vt_h = b.addTranslateC(.{
77+
.root_source_file = b.path("include/ghostty/vt.h"),
78+
.target = target,
79+
.optimize = optimize,
80+
});
81+
ghostty_vt_h.addIncludePath(b.path("include"));
82+
break :v ghostty_vt_h.createModule();
83+
},
84+
};
85+
86+
try modules.put(b.allocator, key, value);
87+
break :value value;
88+
};
89+
90+
module.addImport(switch (dep) {
91+
.ghostty_h => "ghostty-h",
92+
.ghostty_vt_h => "ghostty-vt-h",
93+
}, value);
94+
}

src/lib/enum.zig

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
3+
24
const Target = @import("target.zig").Target;
35

6+
const std_enums = @import("std_enums.zig");
7+
48
/// Create an enum type with the given keys that is C ABI compatible
59
/// if we're targeting C, otherwise a Zig enum with smallest possible
610
/// backing type.
@@ -92,24 +96,54 @@ test "abi by removing a key" {
9296
}
9397
}
9498

99+
const CheckError = error{ SkipZigTest, TestExpectedEqual, TestUnexpectedResult };
100+
101+
fn ghosttyHEnumCheck(expected: anytype, actual: anytype) CheckError!void {
102+
try std.testing.expectEqual(expected, actual);
103+
}
104+
95105
/// Verify that for every key in enum T, there is a matching declaration in
96106
/// `ghostty.h` with the correct value. This should only ever be called inside a `test`
97107
/// because the `ghostty.h` module is only available then.
98-
pub fn checkGhosttyHEnum(
108+
pub fn checkGhosttyHEnum(comptime T: type, comptime prefix: []const u8) CheckError!void {
109+
// these targets don't support libc, which is required for this test
110+
if (comptime builtin.cpu.arch.isWasm()) return error.SkipZigTest;
111+
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
112+
113+
try checkEnum("ghostty.h", @import("ghostty-h"), T, null, prefix, ghosttyHEnumCheck);
114+
}
115+
116+
/// Verify that for every key in enum T, there is a matching macro in
117+
/// `ghostty/vt.h`. This should only ever be called inside a `test` because the
118+
/// `ghostty/vt.h` module is only available then. The actual value of the macro
119+
/// is not checked as the value of the macro may be more complex than an integer
120+
/// that matches the Zig enum.
121+
pub fn checkGhosttyVtHMacroExists(comptime T: type, comptime prefix: []const u8) CheckError!void {
122+
// these targets don't support libc, which is required for this test
123+
if (comptime builtin.cpu.arch.isWasm()) return error.SkipZigTest;
124+
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
125+
126+
try checkEnum("ghostty/vt.h", @import("ghostty-vt-h"), T, null, prefix, null);
127+
}
128+
129+
fn checkEnum(
130+
comptime name: []const u8,
131+
comptime c: anytype,
99132
comptime T: type,
133+
comptime backint_int_type_: ?type,
100134
comptime prefix: []const u8,
135+
comptime check_: ?fn (anytype, anytype) CheckError!void,
101136
) !void {
102137
const info = @typeInfo(T);
103138

104139
try std.testing.expect(info == .@"enum");
105-
try std.testing.expect(info.@"enum".tag_type == c_int);
140+
if (backint_int_type_) |backing_int_type|
141+
try std.testing.expect(info.@"enum".tag_type == backing_int_type);
106142
try std.testing.expect(info.@"enum".is_exhaustive == true);
107143

108144
@setEvalBranchQuota(100_000);
109145

110-
const c = @import("ghostty.h");
111-
112-
var set: std.EnumSet(T) = .initFull();
146+
var set: std_enums.EnumSet(T) = .initFull();
113147

114148
const enum_fields = info.@"enum".fields;
115149

@@ -124,10 +158,15 @@ pub fn checkGhosttyHEnum(
124158
};
125159

126160
if (@hasDecl(c, expected_name)) {
127-
std.testing.expectEqual(field.value, @field(c, expected_name)) catch |e| {
128-
std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ expected_name, .{});
129-
return e;
130-
};
161+
if (check_) |check| {
162+
check(
163+
field.value,
164+
@field(c, expected_name),
165+
) catch |e| {
166+
std.log.err(@typeName(T) ++ " key " ++ field.name ++ " / " ++ expected_name ++ " failed its check: {t}", .{e});
167+
return e;
168+
};
169+
}
131170

132171
set.remove(@enumFromInt(field.value));
133172
}
@@ -138,7 +177,7 @@ pub fn checkGhosttyHEnum(
138177
while (it.next()) |v| {
139178
var buf: [128]u8 = undefined;
140179
const upper_string = std.ascii.upperString(&buf, @tagName(v));
141-
std.log.err("ghostty.h is missing value for {s}{s}", .{ prefix, upper_string });
180+
std.log.err("{s} is missing value for {s}{s}", .{ name, prefix, upper_string });
142181
}
143182
return e;
144183
};

0 commit comments

Comments
 (0)