This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321const 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");
/// Special commands that can be invoked via CLI flags. These are all/// invoked by using `+<action>` 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, }; } }};
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 Action.detectIter(&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 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); }}
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 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); }}
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 Action.detectIter(&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( Action.Error.MultipleActions, Action.detectIter(&iter), ); }}