ghostty/src/cli/action.zig
Jason Rayne 0ccb7cf353 docs: improve SSH cache CLI action descriptions
- Clarify that +list-ssh-cache shows shell integration cached hosts
- Add note about +clear-ssh-cache command and when to use it

Addresses mitchellh's feedback on action descriptions.
2025-06-25 15:46:18 -07:00

327 lines
11 KiB
Zig

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 list_ssh_cache = @import("list_ssh_cache.zig");
const clear_ssh_cache = @import("clear_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",
/// List hosts cached by SSH shell integration for terminfo installation
@"list-ssh-cache",
/// Clear Ghostty SSH terminfo cache
@"clear-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),
.@"list-ssh-cache" => @import("list_ssh_cache.zig").run(alloc),
.@"clear-ssh-cache" => @import("clear_ssh_cache.zig").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,
.@"list-ssh-cache" => list_ssh_cache.Options,
.@"clear-ssh-cache" => clear_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),
);
}
}