From 1739418f6f6fab3bb7df9c2c84eba91ddabe91b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Jul 2025 08:55:34 -0700 Subject: [PATCH] cli: make the action parser (+foo) generic and reusable --- src/build/bash_completions.zig | 2 +- src/build/fish_completions.zig | 2 +- src/build/mdgen/mdgen.zig | 2 +- src/build/zsh_completions.zig | 2 +- src/cli.zig | 3 +- src/cli/action.zig | 511 +++++++++++++++------------------ src/cli/boo.zig | 2 +- src/cli/crash_report.zig | 2 +- src/cli/edit_config.zig | 2 +- src/cli/ghostty.zig | 290 +++++++++++++++++++ src/cli/help.zig | 2 +- src/cli/list_actions.zig | 2 +- src/cli/list_colors.zig | 2 +- src/cli/list_fonts.zig | 2 +- src/cli/list_keybinds.zig | 2 +- src/cli/list_themes.zig | 2 +- src/cli/show_config.zig | 2 +- src/cli/show_face.zig | 2 +- src/cli/ssh_cache.zig | 2 +- src/cli/validate_config.zig | 2 +- src/global.zig | 7 +- src/helpgen.zig | 2 +- 22 files changed, 549 insertions(+), 298 deletions(-) create mode 100644 src/cli/ghostty.zig diff --git a/src/build/bash_completions.zig b/src/build/bash_completions.zig index ad62ff97d..536cadbc4 100644 --- a/src/build/bash_completions.zig +++ b/src/build/bash_completions.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); -const Action = @import("../cli/action.zig").Action; +const Action = @import("../cli.zig").ghostty.Action; /// A bash completions configuration that contains all the available commands /// and options. diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index 2b2563ee7..0b6c45e1f 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); -const Action = @import("../cli/action.zig").Action; +const Action = @import("../cli.zig").ghostty.Action; /// A fish completions configuration that contains all the available commands /// and options. diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index e7d966323..53ed02067 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -2,7 +2,7 @@ const std = @import("std"); const help_strings = @import("help_strings"); const build_config = @import("../../build_config.zig"); const Config = @import("../../config/Config.zig"); -const Action = @import("../../cli/action.zig").Action; +const Action = @import("../../cli/ghostty.zig").Action; const KeybindAction = @import("../../input/Binding.zig").Action; pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { diff --git a/src/build/zsh_completions.zig b/src/build/zsh_completions.zig index 2ded6d73c..6bddcd285 100644 --- a/src/build/zsh_completions.zig +++ b/src/build/zsh_completions.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); -const Action = @import("../cli/action.zig").Action; +const Action = @import("../cli.zig").ghostty.Action; /// A zsh completions configuration that contains all the available commands /// and options. diff --git a/src/cli.zig b/src/cli.zig index 151e6e648..008ff1ebf 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1,7 +1,8 @@ const diags = @import("cli/diagnostics.zig"); pub const args = @import("cli/args.zig"); -pub const Action = @import("cli/action.zig").Action; +pub const action = @import("cli/action.zig"); +pub const ghostty = @import("cli/ghostty.zig"); pub const CompatibilityHandler = args.CompatibilityHandler; pub const compatibilityRenamed = args.compatibilityRenamed; pub const DiagnosticList = diags.DiagnosticList; diff --git a/src/cli/action.zig b/src/cli/action.zig index 728f36efe..41173a9f1 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -1,320 +1,277 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const help_strings = @import("help_strings"); -const list_fonts = @import("list_fonts.zig"); -const help = @import("help.zig"); -const version = @import("version.zig"); -const list_keybinds = @import("list_keybinds.zig"); -const list_themes = @import("list_themes.zig"); -const list_colors = @import("list_colors.zig"); -const list_actions = @import("list_actions.zig"); -const ssh_cache = @import("ssh_cache.zig"); -const edit_config = @import("edit_config.zig"); -const show_config = @import("show_config.zig"); -const validate_config = @import("validate_config.zig"); -const crash_report = @import("crash_report.zig"); -const show_face = @import("show_face.zig"); -const boo = @import("boo.zig"); +pub const DetectError = error{ + /// Multiple actions were detected. You can specify at most one + /// action on the CLI otherwise the behavior desired is ambiguous. + MultipleActions, -/// Special commands that can be invoked via CLI flags. These are all -/// invoked by using `+` as a CLI flag. The only exception is -/// "version" which can be invoked additionally with `--version`. -pub const Action = enum { - /// Output the version and exit - version, - - /// Output help information for the CLI or configuration - help, - - /// List available fonts - @"list-fonts", - - /// List available keybinds - @"list-keybinds", - - /// List available themes - @"list-themes", - - /// List named RGB colors - @"list-colors", - - /// List keybind actions - @"list-actions", - - /// Manage SSH terminfo cache for automatic remote host setup - @"ssh-cache", - - /// Edit the config file in the configured terminal editor. - @"edit-config", - - /// Dump the config to stdout - @"show-config", - - // Validate passed config file - @"validate-config", - - // Show which font face Ghostty loads a codepoint from. - @"show-face", - - // List, (eventually) view, and (eventually) send crash reports. - @"crash-report", - - // Boo! - boo, - - pub const Error = error{ - /// Multiple actions were detected. You can specify at most one - /// action on the CLI otherwise the behavior desired is ambiguous. - MultipleActions, - - /// An unknown action was specified. - InvalidAction, - }; - - /// This should be returned by actions that want to print the help text. - pub const help_error = error.ActionHelpRequested; - - /// Detect the action from CLI args. - pub fn detectCLI(alloc: Allocator) !?Action { - var iter = try std.process.argsWithAllocator(alloc); - defer iter.deinit(); - return try detectIter(&iter); - } - - /// Detect the action from any iterator, used primarily for tests. - pub fn detectIter(iter: anytype) Error!?Action { - var pending_help: bool = false; - var pending: ?Action = null; - while (iter.next()) |arg| { - // If we see a "-e" and we haven't seen a command yet, then - // we are done looking for commands. This special case enables - // `ghostty -e ghostty +command`. If we've seen a command we - // still want to keep looking because - // `ghostty +command -e +command` is invalid. - if (std.mem.eql(u8, arg, "-e") and pending == null) return null; - - // Special case, --version always outputs the version no - // matter what, no matter what other args exist. - if (std.mem.eql(u8, arg, "--version")) return .version; - - // --help matches "help" but if a subcommand is specified - // then we match the subcommand. - if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - pending_help = true; - continue; - } - - // Commands must start with "+" - if (arg.len == 0 or arg[0] != '+') continue; - if (pending != null) return Error.MultipleActions; - pending = std.meta.stringToEnum(Action, arg[1..]) orelse return Error.InvalidAction; - } - - // If we have an action, we always return that action, even if we've - // seen "--help" or "-h" because the action may have its own help text. - if (pending != null) return pending; - - // If we've seen "--help" or "-h" then we return the help action. - if (pending_help) return .help; - - return pending; - } - - /// Run the action. This returns the exit code to exit with. - pub fn run(self: Action, alloc: Allocator) !u8 { - return self.runMain(alloc) catch |err| switch (err) { - // If help is requested, then we use some comptime trickery - // to find this action in the help strings and output that. - help_error => err: { - inline for (@typeInfo(Action).@"enum".fields) |field| { - // Future note: for now we just output the help text directly - // to stdout. In the future we can style this much prettier - // for all commands by just changing this one place. - - if (std.mem.eql(u8, field.name, @tagName(self))) { - const stdout = std.io.getStdOut().writer(); - const text = @field(help_strings.Action, field.name) ++ "\n"; - stdout.writeAll(text) catch |write_err| { - std.log.warn("failed to write help text: {}\n", .{write_err}); - break :err 1; - }; - - break :err 0; - } - } - - break :err err; - }, - else => err, - }; - } - - fn runMain(self: Action, alloc: Allocator) !u8 { - return switch (self) { - .version => try version.run(alloc), - .help => try help.run(alloc), - .@"list-fonts" => try list_fonts.run(alloc), - .@"list-keybinds" => try list_keybinds.run(alloc), - .@"list-themes" => try list_themes.run(alloc), - .@"list-colors" => try list_colors.run(alloc), - .@"list-actions" => try list_actions.run(alloc), - .@"ssh-cache" => try ssh_cache.run(alloc), - .@"edit-config" => try edit_config.run(alloc), - .@"show-config" => try show_config.run(alloc), - .@"validate-config" => try validate_config.run(alloc), - .@"crash-report" => try crash_report.run(alloc), - .@"show-face" => try show_face.run(alloc), - .boo => try boo.run(alloc), - }; - } - - /// Returns the filename associated with an action. This is a relative - /// path from the root src/ directory. - pub fn file(comptime self: Action) []const u8 { - comptime { - const filename = filename: { - const tag = @tagName(self); - var filename: [tag.len]u8 = undefined; - _ = std.mem.replace(u8, tag, "-", "_", &filename); - break :filename &filename; - }; - - return "cli/" ++ filename ++ ".zig"; - } - } - - /// Returns the options of action. Supports generating shell completions - /// without duplicating the mapping from Action to relevant Option - /// @import(..) declaration. - pub fn options(comptime self: Action) type { - comptime { - return switch (self) { - .version => version.Options, - .help => help.Options, - .@"list-fonts" => list_fonts.Options, - .@"list-keybinds" => list_keybinds.Options, - .@"list-themes" => list_themes.Options, - .@"list-colors" => list_colors.Options, - .@"list-actions" => list_actions.Options, - .@"ssh-cache" => ssh_cache.Options, - .@"edit-config" => edit_config.Options, - .@"show-config" => show_config.Options, - .@"validate-config" => validate_config.Options, - .@"crash-report" => crash_report.Options, - .@"show-face" => show_face.Options, - .boo => boo.Options, - }; - } - } + /// An unknown action was specified. + InvalidAction, }; -test "parse action none" { +/// Detect the action from CLI args. +pub fn detectArgs(comptime E: type, alloc: Allocator) !?E { + var iter = try std.process.argsWithAllocator(alloc); + defer iter.deinit(); + return try detectIter(E, &iter); +} + +/// Detect the action from any iterator. Each iterator value should yield +/// a CLI argument such as "--foo". +/// +/// The comptime type E must be an enum with the available actions. +/// If the type E has a decl `detectSpecialCase`, then it will be called +/// for each argument to allow handling of special cases. The function +/// signature for `detectSpecialCase` should be: +/// +/// fn detectSpecialCase(arg: []const u8) ?SpecialCase(E) +/// +pub fn detectIter( + comptime E: type, + iter: anytype, +) DetectError!?E { + var fallback: ?E = null; + var pending: ?E = null; + while (iter.next()) |arg| { + // Allow handling of special cases. + if (@hasDecl(E, "detectSpecialCase")) special: { + const special = E.detectSpecialCase(arg) orelse break :special; + switch (special) { + .action => |a| return a, + .fallback => |a| fallback = a, + .abort_if_no_action => if (pending == null) return null, + } + } + + // Commands must start with "+" + if (arg.len == 0 or arg[0] != '+') continue; + if (pending != null) return DetectError.MultipleActions; + pending = std.meta.stringToEnum(E, arg[1..]) orelse + return DetectError.InvalidAction; + } + + // If we have an action, we always return that action, even if we've + // seen "--help" or "-h" because the action may have its own help text. + if (pending != null) return pending; + + // If we have no action but we have a fallback, then we return that. + if (fallback) |a| return a; + + return null; +} + +/// The action enum E can implement the decl `detectSpecialCase` to +/// return this enum in order to perform various special case actions. +pub fn SpecialCase(comptime E: type) type { + return union(enum) { + /// Immediately return this action. + action: E, + + /// Return this action if no other action is found. + fallback: E, + + /// If there is no pending action (we haven't seen an action yet) + /// then we should return no action. This is kind of weird but is + /// a special case to allow "-e" in Ghostty. + abort_if_no_action, + }; +} + +test "detect direct match" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; var iter = try std.process.ArgIteratorGeneral(.{}).init( alloc, - "--a=42 --b --b-f=false", + "+foo", ); defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action == null); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); } -test "parse action version" { +test "detect invalid match" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--a=42 --b --b-f=false --version", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--c=84 --d --version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+invalid", + ); + defer iter.deinit(); + try testing.expectError( + DetectError.InvalidAction, + detectIter(Enum, &iter), + ); } -test "parse action plus" { +test "detect multiple actions" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--a=42 --b --b-f=false +version", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "+version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--c=84 --d +version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+foo +bar", + ); + defer iter.deinit(); + try testing.expectError( + DetectError.MultipleActions, + detectIter(Enum, &iter), + ); } -test "parse action plus ignores -e" { +test "detect no match" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--some-flag", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expect(result == null); +} + +test "detect special case action" { + const testing = std.testing; + const alloc = testing.allocator; + const Enum = enum { + foo, + bar, + + fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) { + return if (std.mem.eql(u8, arg, "--special")) + .{ .action = .foo } + else + null; + } + }; { var iter = try std.process.ArgIteratorGeneral(.{}).init( alloc, - "--a=42 -e +version", + "--special +bar", ); defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action == null); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); } { var iter = try std.process.ArgIteratorGeneral(.{}).init( alloc, - "+list-fonts --a=42 -e +version", + "+bar --special", ); defer iter.deinit(); - try testing.expectError( - Action.Error.MultipleActions, - Action.detectIter(&iter), + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+bar", ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.bar, result.?); + } +} + +test "detect special case fallback" { + const testing = std.testing; + const alloc = testing.allocator; + const Enum = enum { + foo, + bar, + + fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) { + return if (std.mem.eql(u8, arg, "--special")) + .{ .fallback = .foo } + else + null; + } + }; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--special", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+bar --special", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.bar, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--special +bar", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.bar, result.?); + } +} + +test "detect special case abort_if_no_action" { + const testing = std.testing; + const alloc = testing.allocator; + const Enum = enum { + foo, + bar, + + fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) { + return if (std.mem.eql(u8, arg, "-e")) + .abort_if_no_action + else + null; + } + }; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "-e", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expect(result == null); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+foo -e", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "-e +bar", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expect(result == null); } } diff --git a/src/cli/boo.zig b/src/cli/boo.zig index 47c8ab741..72b282ef6 100644 --- a/src/cli/boo.zig +++ b/src/cli/boo.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Allocator = std.mem.Allocator; const help_strings = @import("help_strings"); const vaxis = @import("vaxis"); diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index ff8509797..c6a383563 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Config = @import("../config.zig").Config; const crash = @import("../crash/main.zig"); diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index 3be88e090..dd09d7e2f 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const assert = std.debug.assert; const args = @import("args.zig"); const Allocator = std.mem.Allocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const configpkg = @import("../config.zig"); const internal_os = @import("../os/main.zig"); const Config = configpkg.Config; diff --git a/src/cli/ghostty.zig b/src/cli/ghostty.zig new file mode 100644 index 000000000..c1b661f70 --- /dev/null +++ b/src/cli/ghostty.zig @@ -0,0 +1,290 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const help_strings = @import("help_strings"); +const actionpkg = @import("action.zig"); +const SpecialCase = actionpkg.SpecialCase; + +const list_fonts = @import("list_fonts.zig"); +const help = @import("help.zig"); +const version = @import("version.zig"); +const list_keybinds = @import("list_keybinds.zig"); +const list_themes = @import("list_themes.zig"); +const list_colors = @import("list_colors.zig"); +const list_actions = @import("list_actions.zig"); +const ssh_cache = @import("ssh_cache.zig"); +const edit_config = @import("edit_config.zig"); +const show_config = @import("show_config.zig"); +const validate_config = @import("validate_config.zig"); +const crash_report = @import("crash_report.zig"); +const show_face = @import("show_face.zig"); +const boo = @import("boo.zig"); + +/// Special commands that can be invoked via CLI flags. These are all +/// invoked by using `+` as a CLI flag. The only exception is +/// "version" which can be invoked additionally with `--version`. +pub const Action = enum { + /// Output the version and exit + version, + + /// Output help information for the CLI or configuration + help, + + /// List available fonts + @"list-fonts", + + /// List available keybinds + @"list-keybinds", + + /// List available themes + @"list-themes", + + /// List named RGB colors + @"list-colors", + + /// List keybind actions + @"list-actions", + + /// Manage SSH terminfo cache for automatic remote host setup + @"ssh-cache", + + /// Edit the config file in the configured terminal editor. + @"edit-config", + + /// Dump the config to stdout + @"show-config", + + // Validate passed config file + @"validate-config", + + // Show which font face Ghostty loads a codepoint from. + @"show-face", + + // List, (eventually) view, and (eventually) send crash reports. + @"crash-report", + + // Boo! + boo, + + pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) { + // If we see a "-e" and we haven't seen a command yet, then + // we are done looking for commands. This special case enables + // `ghostty -e ghostty +command`. If we've seen a command we + // still want to keep looking because + // `ghostty +command -e +command` is invalid. + if (std.mem.eql(u8, arg, "-e")) return .abort_if_no_action; + + // Special case, --version always outputs the version no + // matter what, no matter what other args exist. + if (std.mem.eql(u8, arg, "--version")) { + return .{ .action = .version }; + } + + // --help matches "help" but if a subcommand is specified + // then we match the subcommand. + if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + return .{ .fallback = .help }; + } + + return null; + } + + /// This should be returned by actions that want to print the help text. + pub const help_error = error.ActionHelpRequested; + + /// Run the action. This returns the exit code to exit with. + pub fn run(self: Action, alloc: Allocator) !u8 { + return self.runMain(alloc) catch |err| switch (err) { + // If help is requested, then we use some comptime trickery + // to find this action in the help strings and output that. + help_error => err: { + inline for (@typeInfo(Action).@"enum".fields) |field| { + // Future note: for now we just output the help text directly + // to stdout. In the future we can style this much prettier + // for all commands by just changing this one place. + + if (std.mem.eql(u8, field.name, @tagName(self))) { + const stdout = std.io.getStdOut().writer(); + const text = @field(help_strings.Action, field.name) ++ "\n"; + stdout.writeAll(text) catch |write_err| { + std.log.warn("failed to write help text: {}\n", .{write_err}); + break :err 1; + }; + + break :err 0; + } + } + + break :err err; + }, + else => err, + }; + } + + fn runMain(self: Action, alloc: Allocator) !u8 { + return switch (self) { + .version => try version.run(alloc), + .help => try help.run(alloc), + .@"list-fonts" => try list_fonts.run(alloc), + .@"list-keybinds" => try list_keybinds.run(alloc), + .@"list-themes" => try list_themes.run(alloc), + .@"list-colors" => try list_colors.run(alloc), + .@"list-actions" => try list_actions.run(alloc), + .@"ssh-cache" => try ssh_cache.run(alloc), + .@"edit-config" => try edit_config.run(alloc), + .@"show-config" => try show_config.run(alloc), + .@"validate-config" => try validate_config.run(alloc), + .@"crash-report" => try crash_report.run(alloc), + .@"show-face" => try show_face.run(alloc), + .boo => try boo.run(alloc), + }; + } + + /// Returns the filename associated with an action. This is a relative + /// path from the root src/ directory. + pub fn file(comptime self: Action) []const u8 { + comptime { + const filename = filename: { + const tag = @tagName(self); + var filename: [tag.len]u8 = undefined; + _ = std.mem.replace(u8, tag, "-", "_", &filename); + break :filename &filename; + }; + + return "cli/" ++ filename ++ ".zig"; + } + } + + /// Returns the options of action. Supports generating shell completions + /// without duplicating the mapping from Action to relevant Option + /// @import(..) declaration. + pub fn options(comptime self: Action) type { + comptime { + return switch (self) { + .version => version.Options, + .help => help.Options, + .@"list-fonts" => list_fonts.Options, + .@"list-keybinds" => list_keybinds.Options, + .@"list-themes" => list_themes.Options, + .@"list-colors" => list_colors.Options, + .@"list-actions" => list_actions.Options, + .@"ssh-cache" => ssh_cache.Options, + .@"edit-config" => edit_config.Options, + .@"show-config" => show_config.Options, + .@"validate-config" => validate_config.Options, + .@"crash-report" => crash_report.Options, + .@"show-face" => show_face.Options, + .boo => boo.Options, + }; + } + } +}; + +test "parse action none" { + const testing = std.testing; + const alloc = testing.allocator; + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action == null); +} + +test "parse action version" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 --b --b-f=false --version", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--c=84 --d --version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } +} + +test "parse action plus" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 --b --b-f=false +version", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--c=84 --d +version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } +} + +test "parse action plus ignores -e" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 -e +version", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action == null); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+list-fonts --a=42 -e +version", + ); + defer iter.deinit(); + try testing.expectError( + actionpkg.DetectError.MultipleActions, + actionpkg.detectIter(Action, &iter), + ); + } +} diff --git a/src/cli/help.zig b/src/cli/help.zig index 6c989fd0c..0528dc1c2 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; // Note that this options struct doesn't implement the `help` decl like other // actions. That is because the help command is special and wants to handle its diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index 1d17873cc..6f5ce06a2 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -1,6 +1,6 @@ const std = @import("std"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Allocator = std.mem.Allocator; const helpgen_actions = @import("../input/helpgen_actions.zig"); diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig index bfe17df7c..e43a43c86 100644 --- a/src/cli/list_colors.zig +++ b/src/cli/list_colors.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const x11_color = @import("../terminal/main.zig").x11_color; diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index e8a010ecd..58246d3ad 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const font = @import("../font/main.zig"); diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index f84d540c3..94f445eea 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Arena = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; const configpkg = @import("../config.zig"); diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index e80a92286..b85f98445 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -1,7 +1,7 @@ const std = @import("std"); const inputpkg = @import("../input.zig"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Config = @import("../config/Config.zig"); const themepkg = @import("../config/theme.zig"); const tui = @import("tui.zig"); diff --git a/src/cli/show_config.zig b/src/cli/show_config.zig index cbcd2486d..3f22c75c2 100644 --- a/src/cli/show_config.zig +++ b/src/cli/show_config.zig @@ -1,7 +1,7 @@ const std = @import("std"); const args = @import("args.zig"); const Allocator = std.mem.Allocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const configpkg = @import("../config.zig"); const Config = configpkg.Config; diff --git a/src/cli/show_face.zig b/src/cli/show_face.zig index b7f039dc8..e3b596bcd 100644 --- a/src/cli/show_face.zig +++ b/src/cli/show_face.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const diagnostics = @import("diagnostics.zig"); const font = @import("../font/main.zig"); diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index c8e2e1123..1099f0112 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -3,7 +3,7 @@ const fs = std.fs; const Allocator = std.mem.Allocator; const xdg = @import("../os/xdg.zig"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; pub const Entry = @import("ssh-cache/Entry.zig"); pub const DiskCache = @import("ssh-cache/DiskCache.zig"); diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig index 5bc6ff406..114843e9a 100644 --- a/src/cli/validate_config.zig +++ b/src/cli/validate_config.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Config = @import("../config.zig").Config; const cli = @import("../cli.zig"); diff --git a/src/global.zig b/src/global.zig index 668d2faec..e68ec7f74 100644 --- a/src/global.zig +++ b/src/global.zig @@ -30,7 +30,7 @@ pub const GlobalState = struct { gpa: ?GPA, alloc: std.mem.Allocator, - action: ?cli.Action, + action: ?cli.ghostty.Action, logging: Logging, rlimits: ResourceLimits = .{}, @@ -92,7 +92,10 @@ pub const GlobalState = struct { unreachable; // We first try to parse any action that we may be executing. - self.action = try cli.Action.detectCLI(self.alloc); + self.action = try cli.action.detectArgs( + cli.ghostty.Action, + self.alloc, + ); // If we have an action executing, we disable logging by default // since we write to stderr we don't want logs messing up our diff --git a/src/helpgen.zig b/src/helpgen.zig index 560e5ce29..e1628c218 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -4,7 +4,7 @@ const std = @import("std"); const Config = @import("config/Config.zig"); -const Action = @import("cli/action.zig").Action; +const Action = @import("cli.zig").ghostty.Action; const KeybindAction = @import("input/Binding.zig").Action; pub fn main() !void {