summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMitchell Hashimoto <mitchell.hashimoto@gmail.com>2024-01-20 11:48:35 -0800
committerGitHub <noreply@github.com>2024-01-20 11:48:35 -0800
commitf9996d46eca2956041dedecc543253bc541cef03 (patch)
treebedd1dd7614506ef49232a3fe4c82bea8e2cc855 /src
parenteff62fa915e145dbd4cb0869d91e28d93b3b042a (diff)
parent7595b9b4bf75a243940f64da957e5c8a8457f191 (diff)
Merge pull request #1338 from mitchellh/help-rebase
Add `--help` to the Ghostty CLI
Diffstat (limited to 'src')
-rw-r--r--src/cli/action.zig69
-rw-r--r--src/cli/args.zig11
-rw-r--r--src/cli/help.zig71
-rw-r--r--src/cli/list_colors.zig7
-rw-r--r--src/cli/list_fonts.zig7
-rw-r--r--src/cli/list_keybinds.zig7
-rw-r--r--src/cli/list_themes.zig7
-rw-r--r--src/cli/version.zig28
-rw-r--r--src/helpgen.zig167
9 files changed, 372 insertions, 2 deletions
diff --git a/src/cli/action.zig b/src/cli/action.zig
index f97a5d2cf..2e3afb1c0 100644
--- a/src/cli/action.zig
+++ b/src/cli/action.zig
@@ -1,7 +1,9 @@
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");
@@ -14,6 +16,9 @@ pub const Action = enum {
/// Output the version and exit
version,
+ /// Output help information for the CLI or configuration
+ help,
+
/// List available fonts
@"list-fonts",
@@ -35,6 +40,9 @@ pub const Action = enum {
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);
@@ -44,31 +52,90 @@ pub const Action = enum {
/// 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| {
// 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(),
+ .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),
};
}
+
+ /// 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";
+ }
+ }
};
test "parse action none" {
diff --git a/src/cli/args.zig b/src/cli/args.zig
index c226493f9..773457cf8 100644
--- a/src/cli/args.zig
+++ b/src/cli/args.zig
@@ -77,6 +77,17 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return;
}
+ // If the destination supports help then we check for it, call
+ // the help function and return.
+ if (@hasDecl(T, "help")) {
+ if (mem.eql(u8, arg, "--help") or
+ mem.eql(u8, arg, "-h"))
+ {
+ try dst.help();
+ return;
+ }
+ }
+
if (mem.startsWith(u8, arg, "--")) {
var key: []const u8 = arg[2..];
const value: ?[]const u8 = value: {
diff --git a/src/cli/help.zig b/src/cli/help.zig
new file mode 100644
index 000000000..e75b1b617
--- /dev/null
+++ b/src/cli/help.zig
@@ -0,0 +1,71 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const args = @import("args.zig");
+const Action = @import("action.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 own logic around help detection.
+pub const Options = struct {
+ /// This must be registered so that it isn't an error to pass `--help`
+ help: bool = false,
+
+ pub fn deinit(self: Options) void {
+ _ = self;
+ }
+};
+
+/// The `help` command shows general help about Ghostty. You can also
+/// specify `--help` or `-h` along with any action such as `+list-themes`
+/// to see help for a specific action.
+pub fn run(alloc: Allocator) !u8 {
+ var opts: Options = .{};
+ defer opts.deinit();
+
+ {
+ var iter = try std.process.argsWithAllocator(alloc);
+ defer iter.deinit();
+ try args.parse(Options, alloc, &opts, &iter);
+ }
+
+ const stdout = std.io.getStdOut().writer();
+ try stdout.writeAll(
+ \\Usage: ghostty [+action] [options]
+ \\
+ \\Run the Ghostty terminal emulator or a specific helper action.
+ \\
+ \\If no `+action` is specified, run the Ghostty terminal emulator.
+ \\All configuration keys are available as command line options.
+ \\To specify a configuration key, use the `--<key>=<value>` syntax
+ \\where key and value are the same format you'd put into a configuration
+ \\file. For example, `--font-size=12` or `--font-family="Fira Code"`.
+ \\
+ \\To see a list of all available configuration options, please see
+ \\the `src/config/Config.zig` file. A future update will allow seeing
+ \\the list of configuration options from the command line.
+ \\
+ \\A special command line argument `-e <command>` can be used to run
+ \\the specific command inside the terminal emulator. For example,
+ \\`ghostty -e top` will run the `top` command inside the terminal.
+ \\
+ \\On macOS, launching the terminal emulator from the CLI is not
+ \\supported and only actions are supported.
+ \\
+ \\Available actions:
+ \\
+ \\
+ );
+
+ inline for (@typeInfo(Action).Enum.fields) |field| {
+ try stdout.print(" +{s}\n", .{field.name});
+ }
+
+ try stdout.writeAll(
+ \\
+ \\Specify `+<action> --help` to see the help for a specific action,
+ \\where `<action>` is one of actions listed below.
+ \\
+ );
+
+ return 0;
+}
diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig
index 2b6dd1d0e..447f70552 100644
--- a/src/cli/list_colors.zig
+++ b/src/cli/list_colors.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const Action = @import("action.zig").Action;
const args = @import("args.zig");
const x11_color = @import("../terminal/main.zig").x11_color;
@@ -6,6 +7,12 @@ pub const Options = struct {
pub fn deinit(self: Options) void {
_ = self;
}
+
+ /// Enables "-h" and "--help" to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
};
/// The "list-colors" command is used to list all the named RGB colors in
diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig
index 0e48bf0f9..b49e43a30 100644
--- a/src/cli/list_fonts.zig
+++ b/src/cli/list_fonts.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
+const Action = @import("action.zig").Action;
const args = @import("args.zig");
const font = @import("../font/main.zig");
@@ -26,6 +27,12 @@ pub const Config = struct {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
+
+ /// Enables "-h" and "--help" to work.
+ pub fn help(self: Config) !void {
+ _ = self;
+ return Action.help_error;
+ }
};
/// The list-fonts command is used to list all the available fonts for Ghostty.
diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig
index 8d9585be9..15df1815e 100644
--- a/src/cli/list_keybinds.zig
+++ b/src/cli/list_keybinds.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const inputpkg = @import("../input.zig");
const args = @import("args.zig");
+const Action = @import("action.zig").Action;
const Arena = std.heap.ArenaAllocator;
const Allocator = std.mem.Allocator;
const Config = @import("../config/Config.zig");
@@ -13,6 +14,12 @@ pub const Options = struct {
pub fn deinit(self: Options) void {
_ = self;
}
+
+ /// Enables "-h" and "--help" to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
};
/// The "list-keybinds" command is used to list all the available keybinds
diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig
index be3a41c14..98c3859c6 100644
--- a/src/cli/list_themes.zig
+++ b/src/cli/list_themes.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const inputpkg = @import("../input.zig");
const args = @import("args.zig");
+const Action = @import("action.zig").Action;
const Arena = std.heap.ArenaAllocator;
const Allocator = std.mem.Allocator;
const Config = @import("../config/Config.zig");
@@ -10,6 +11,12 @@ pub const Options = struct {
pub fn deinit(self: Options) void {
_ = self;
}
+
+ /// Enables "-h" and "--help" to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
};
/// The "list-themes" command is used to list all the available themes
diff --git a/src/cli/version.zig b/src/cli/version.zig
index b3395f4cd..fb12faa2e 100644
--- a/src/cli/version.zig
+++ b/src/cli/version.zig
@@ -1,10 +1,36 @@
const std = @import("std");
+const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const xev = @import("xev");
const renderer = @import("../renderer.zig");
+const args = @import("args.zig");
+const Action = @import("action.zig").Action;
+
+pub const Options = struct {
+ pub fn deinit(self: Options) void {
+ _ = self;
+ }
+
+ /// Enables "-h" and "--help" to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
+};
+
+/// The `version` command is used to display information
+/// about Ghostty.
+pub fn run(alloc: Allocator) !u8 {
+ var opts: Options = .{};
+ defer opts.deinit();
+
+ {
+ var iter = try std.process.argsWithAllocator(alloc);
+ defer iter.deinit();
+ try args.parse(Options, alloc, &opts, &iter);
+ }
-pub fn run() !u8 {
const stdout = std.io.getStdOut().writer();
try stdout.print("Ghostty {s}\n\n", .{build_config.version_string});
try stdout.print("Build Config\n", .{});
diff --git a/src/helpgen.zig b/src/helpgen.zig
new file mode 100644
index 000000000..4447346f4
--- /dev/null
+++ b/src/helpgen.zig
@@ -0,0 +1,167 @@
+//! This program is used to generate the help strings from the configuration
+//! file and CLI actions for Ghostty. These can then be used to generate
+//! help, docs, website, etc.
+
+const std = @import("std");
+const ziglyph = @import("ziglyph");
+const Config = @import("config/Config.zig");
+const Action = @import("cli/action.zig").Action;
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const alloc = gpa.allocator();
+
+ const stdout = std.io.getStdOut().writer();
+ try stdout.writeAll(
+ \\// THIS FILE IS AUTO GENERATED
+ \\
+ \\
+ );
+
+ try genConfig(alloc, stdout);
+ try genActions(alloc, stdout);
+}
+
+fn genConfig(alloc: std.mem.Allocator, writer: anytype) !void {
+ var ast = try std.zig.Ast.parse(alloc, @embedFile("config/Config.zig"), .zig);
+ defer ast.deinit(alloc);
+
+ try writer.writeAll(
+ \\/// Configuration help
+ \\pub const Config = struct {
+ \\
+ \\
+ );
+
+ inline for (@typeInfo(Config).Struct.fields) |field| {
+ if (field.name[0] == '_') continue;
+ try genConfigField(alloc, writer, ast, field.name);
+ }
+
+ try writer.writeAll("};\n");
+}
+
+fn genConfigField(
+ alloc: std.mem.Allocator,
+ writer: anytype,
+ ast: std.zig.Ast,
+ comptime field: []const u8,
+) !void {
+ const tokens = ast.tokens.items(.tag);
+ for (tokens, 0..) |token, i| {
+ // We only care about identifiers that are preceded by doc comments.
+ if (token != .identifier) continue;
+ if (tokens[i - 1] != .doc_comment) continue;
+
+ // Identifier may have @"" so we strip that.
+ const name = ast.tokenSlice(@intCast(i));
+ const key = if (name[0] == '@') name[2 .. name.len - 1] else name;
+ if (!std.mem.eql(u8, key, field)) continue;
+
+ const comment = try extractDocComments(alloc, ast, @intCast(i - 1), tokens);
+ try writer.writeAll("pub const ");
+ try writer.writeAll(name);
+ try writer.writeAll(": [:0]const u8 = \n");
+ try writer.writeAll(comment);
+ try writer.writeAll("\n");
+ break;
+ }
+}
+
+fn genActions(alloc: std.mem.Allocator, writer: anytype) !void {
+ try writer.writeAll(
+ \\
+ \\/// Actions help
+ \\pub const Action = struct {
+ \\
+ \\
+ );
+
+ inline for (@typeInfo(Action).Enum.fields) |field| {
+ const action_file = comptime action_file: {
+ const action = @field(Action, field.name);
+ break :action_file action.file();
+ };
+
+ var ast = try std.zig.Ast.parse(alloc, @embedFile(action_file), .zig);
+ defer ast.deinit(alloc);
+ const tokens: []std.zig.Token.Tag = ast.tokens.items(.tag);
+
+ for (tokens, 0..) |token, i| {
+ // We're looking for a function named "run".
+ if (token != .keyword_fn) continue;
+ if (!std.mem.eql(u8, ast.tokenSlice(@intCast(i + 1)), "run")) continue;
+
+ // The function must be preceded by a doc comment.
+ if (tokens[i - 2] != .doc_comment) {
+ std.debug.print(
+ "doc comment must be present on run function of the {s} action!",
+ .{field.name},
+ );
+ std.process.exit(1);
+ }
+
+ const comment = try extractDocComments(alloc, ast, @intCast(i - 2), tokens);
+ try writer.writeAll("pub const @\"");
+ try writer.writeAll(field.name);
+ try writer.writeAll("\" = \n");
+ try writer.writeAll(comment);
+ try writer.writeAll("\n\n");
+ break;
+ }
+ }
+
+ try writer.writeAll("};\n");
+}
+
+fn extractDocComments(
+ alloc: std.mem.Allocator,
+ ast: std.zig.Ast,
+ index: std.zig.Ast.TokenIndex,
+ tokens: []std.zig.Token.Tag,
+) ![]const u8 {
+ // Find the first index of the doc comments. The doc comments are
+ // always stacked on top of each other so we can just go backwards.
+ const start_idx: usize = start_idx: for (0..index) |i| {
+ const reverse_i = index - i - 1;
+ const token = tokens[reverse_i];
+ if (token != .doc_comment) break :start_idx reverse_i + 1;
+ } else unreachable;
+
+ // Go through and build up the lines.
+ var lines = std.ArrayList([]const u8).init(alloc);
+ defer lines.deinit();
+ for (start_idx..index + 1) |i| {
+ const token = tokens[i];
+ if (token != .doc_comment) break;
+ try lines.append(ast.tokenSlice(@intCast(i))[3..]);
+ }
+
+ // Convert the lines to a multiline string.
+ var buffer = std.ArrayList(u8).init(alloc);
+ const writer = buffer.writer();
+ const prefix = findCommonPrefix(lines);
+ for (lines.items) |line| {
+ try writer.writeAll(" \\\\");
+ try writer.writeAll(line[@min(prefix, line.len)..]);
+ try writer.writeAll("\n");
+ }
+ try writer.writeAll(";\n");
+
+ return buffer.toOwnedSlice();
+}
+
+fn findCommonPrefix(lines: std.ArrayList([]const u8)) usize {
+ var m: usize = std.math.maxInt(usize);
+ for (lines.items) |line| {
+ var n: usize = std.math.maxInt(usize);
+ for (line, 0..) |c, i| {
+ if (c != ' ') {
+ n = i;
+ break;
+ }
+ }
+ m = @min(m, n);
+ }
+ return m;
+}