diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f59a5bd67..b49fd3b4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/src/config.zig b/src/config.zig index 314fb49ee..289b6a811 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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; diff --git a/src/config/Config.zig b/src/config/Config.zig index d32740783..facbdb21d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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, diff --git a/src/config/formatter.zig b/src/config/formatter.zig index dcf99167d..00d946f88 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -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; diff --git a/src/config/formatter_file.zig b/src/config/formatter_file.zig new file mode 100644 index 000000000..37beb7b54 --- /dev/null +++ b/src/config/formatter_file.zig @@ -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()}); +} diff --git a/src/input/key.zig b/src/input/key.zig index a929a0323..9c04f01e0 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -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; diff --git a/src/lib/enum.zig b/src/lib/enum.zig index bdec2ab88..f40a40b54 100644 --- a/src/lib/enum.zig +++ b/src/lib/enum.zig @@ -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"); diff --git a/src/renderer/size.zig b/src/renderer/size.zig index 7a022cdb4..6cf05f58a 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -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; diff --git a/src/terminal/c/build_info.zig b/src/terminal/c/build_info.zig index d65a3b562..312d94fec 100644 --- a/src/terminal/c/build_info.zig +++ b/src/terminal/c/build_info.zig @@ -43,6 +43,7 @@ pub fn get( } return switch (data) { + .invalid => .invalid_value, inline else => |comptime_data| getTyped( comptime_data, @ptrCast(@alignCast(out)), diff --git a/src/terminal/c/cell.zig b/src/terminal/c/cell.zig index 2ff9b35f9..171867048 100644 --- a/src/terminal/c/cell.zig +++ b/src/terminal/c/cell.zig @@ -110,6 +110,7 @@ pub fn get( } return switch (data) { + .invalid => .invalid_value, inline else => |comptime_data| getTyped( cell_, comptime_data, diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index 2cdb765af..616a438d9 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -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); } diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index c4cdaad3b..648e448fd 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -76,6 +76,7 @@ pub fn commandData( } return switch (data) { + .invalid => false, inline else => |comptime_data| commandDataTyped( command_, comptime_data, diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index f8a107979..020dbc202 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -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, diff --git a/src/terminal/c/row.zig b/src/terminal/c/row.zig index 6614d922e..7b765b56a 100644 --- a/src/terminal/c/row.zig +++ b/src/terminal/c/row.zig @@ -73,6 +73,7 @@ pub fn get( } return switch (data) { + .invalid => .invalid_value, inline else => |comptime_data| getTyped( row_, comptime_data, diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 97b76985d..2dd56a519 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -196,6 +196,7 @@ pub fn get( } return switch (data) { + .invalid => .invalid_value, inline else => |comptime_data| getTyped( terminal_, comptime_data, diff --git a/src/terminal/mouse.zig b/src/terminal/mouse.zig index e72e166f7..5286ab856 100644 --- a/src/terminal/mouse.zig +++ b/src/terminal/mouse.zig @@ -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_"); } }; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 0086e48bc..920ce3f7e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -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_"); } };