mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-18 09:46:07 +03:00
cli: make the action parser (+foo) generic and reusable
This commit is contained in:
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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 `+<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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;
|
||||
|
290
src/cli/ghostty.zig
Normal file
290
src/cli/ghostty.zig
Normal file
@ -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 `+<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 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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user