From f9ac37cdf7ac173ba82fee5a5c4201c7443e25f9 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 16 Jan 2024 10:00:36 -0600 Subject: [PATCH 1/5] Generate help strings from doc comments Doc strings attached to fields of the Config struct and doc strings attached to the run function of actions will be used to generate Zig code that makes those doc strings available to be used at runtime. Based on PR #853 by @Raiden1411 --- build.zig | 35 ++++++++ src/cli/version.zig | 2 + src/generate_help_strings.zig | 165 ++++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/generate_help_strings.zig diff --git a/build.zig b/build.zig index 0baf6a245..6505f8aec 100644 --- a/build.zig +++ b/build.zig @@ -197,6 +197,8 @@ pub fn build(b: *std.Build) !void { .{version}, )); + createHelp(b); + // Exe if (exe_) |exe| { exe.root_module.addOptions("build_options", exe_options); @@ -1098,9 +1100,42 @@ fn addDeps( } } + addHelp(step); + return static_libs; } +var generate_help_step: *std.Build.Step.Run = undefined; +var help_strings: std.Build.LazyPath = undefined; + +/// Generate help files +fn createHelp(b: *std.Build) void { + const generate_help = b.addExecutable(.{ + .name = "generate_help", + .root_source_file = .{ .path = "src/generate_help_strings.zig" }, + .target = b.host, + }); + + generate_help_step = b.addRunArtifact(generate_help); + generate_help_step.step.dependOn(&generate_help.step); + + help_strings = generate_help_step.addOutputFileArg("help_strings.zig"); + + if (builtin.target.isDarwin()) { + const generated = b.option([]const u8, "help_strings", "generated help file") orelse "help_strings"; + const write_file = b.addWriteFiles(); + help_strings = write_file.addCopyFile(help_strings, generated); + } +} + +/// Add the generated help files to the build. +fn addHelp(step: *std.Build.Step.Compile) void { + step.step.dependOn(&generate_help_step.step); + step.root_module.addAnonymousImport("help_strings", .{ + .root_source_file = help_strings, + }); +} + fn benchSteps( b: *std.Build, target: std.Build.ResolvedTarget, diff --git a/src/cli/version.zig b/src/cli/version.zig index b3395f4cd..dca20ad2f 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -4,6 +4,8 @@ const build_config = @import("../build_config.zig"); const xev = @import("xev"); const renderer = @import("../renderer.zig"); +/// The `version` command is used to display information +/// about Ghostty. pub fn run() !u8 { const stdout = std.io.getStdOut().writer(); try stdout.print("Ghostty {s}\n\n", .{build_config.version_string}); diff --git a/src/generate_help_strings.zig b/src/generate_help_strings.zig new file mode 100644 index 000000000..1b4ed35ca --- /dev/null +++ b/src/generate_help_strings.zig @@ -0,0 +1,165 @@ +const std = @import("std"); +const ziglyph = @import("ziglyph"); +const Action = @import("cli/action.zig").Action; +const Config = @import("config/Config.zig"); + +pub fn searchConfigAst(alloc: std.mem.Allocator, output: std.fs.File) !void { + var ast = try std.zig.Ast.parse(alloc, @embedFile("config/Config.zig"), .zig); + defer ast.deinit(alloc); + + const config: Config = .{}; + + const tokens = ast.tokens.items(.tag); + + var set = std.StringHashMap(bool).init(alloc); + defer set.deinit(); + + try output.writeAll( + \\//THIS FILE IS AUTO GENERATED + \\//DO NOT MAKE ANY CHANGES TO THIS FILE! + ); + + try output.writeAll("\n\n"); + + inline for (@typeInfo(@TypeOf(config)).Struct.fields) |field| { + if (field.name[0] != '_') try set.put(field.name, false); + } + + var index: u32 = 0; + while (true) : (index += 1) { + if (index >= tokens.len) break; + const token = tokens[index]; + + if (token == .identifier) { + const slice = ast.tokenSlice(index); + // We need this check because the ast grabs the identifier with @"" in case it's used. + const key = if (slice[0] == '@') slice[2 .. slice.len - 1] else slice; + + if (key[0] == '_') continue; + + if (set.get(key)) |value| { + if (value) continue; + if (tokens[index - 1] != .doc_comment) continue; + + const comment = try consumeDocComments(alloc, ast, index - 1, &tokens); + const prop_type = ": " ++ "[:0]const u8 " ++ "= " ++ "\n"; + + try output.writeAll(slice); + try output.writeAll(prop_type); + // const concat = try std.mem.concat(self.alloc, u8, &.{ slice, prop_type }); + // try output.writeAll(concat); + try output.writeAll(comment); + try output.writeAll("\n\n"); + + try set.put(key, true); + } + } + if (token == .eof) break; + } +} + +fn actionPath(comptime action: Action) []const u8 { + return switch (action) { + .version => "cli/version.zig", + .@"list-fonts" => "cli/list_fonts.zig", + .@"list-keybinds" => "cli/list_keybinds.zig", + .@"list-themes" => "cli/list_themes.zig", + .@"list-colors" => "cli/list_colors.zig", + }; +} + +pub fn searchActionsAst(alloc: std.mem.Allocator, output: std.fs.File) !void { + inline for (@typeInfo(Action).Enum.fields) |field| { + const action = comptime std.meta.stringToEnum(Action, field.name).?; + + var ast = try std.zig.Ast.parse(alloc, @embedFile(comptime actionPath(action)), .zig); + const tokens = ast.tokens.items(.tag); + + var index: u32 = 0; + while (true) : (index += 1) { + if (tokens[index] == .keyword_fn) { + if (std.mem.eql(u8, ast.tokenSlice(index + 1), "run")) { + if (tokens[index - 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 consumeDocComments(alloc, ast, index - 2, &tokens); + const prop_type = "@\"+" ++ field.name ++ "\"" ++ ": " ++ "[:0]const u8 " ++ "= " ++ "\n"; + + try output.writeAll(prop_type); + try output.writeAll(comment); + try output.writeAll("\n\n"); + break; + } + } + } + } +} + +fn consumeDocComments(alloc: std.mem.Allocator, ast: std.zig.Ast, index: std.zig.Ast.TokenIndex, toks: anytype) ![]const u8 { + var lines = std.ArrayList([]const u8).init(alloc); + defer lines.deinit(); + + const tokens = toks.*; + var current_idx = index; + + // We iterate backwards because the doc_comment tokens should be on top of each other in case there are any. + while (true) : (current_idx -= 1) { + const token = tokens[current_idx]; + + if (token != .doc_comment) break; + // Insert at 0 so that we don't have the text in reverse. + try lines.insert(0, ast.tokenSlice(current_idx)[3..]); + } + + const prefix = findCommonPrefix(lines); + + var buffer = std.ArrayList(u8).init(alloc); + const writer = buffer.writer(); + + 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; +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var arena = std.heap.ArenaAllocator.init(gpa.allocator()); + const alloc = arena.allocator(); + + const args = try std.process.argsAlloc(alloc); + defer std.process.argsFree(alloc, args); + + if (args.len != 2) { + std.debug.print("invalid number of arguments provided!", .{}); + std.process.exit(1); + } + + const path = args[1]; + + var output = try std.fs.cwd().createFile(path, .{}); + defer output.close(); + + try searchConfigAst(alloc, output); + try searchActionsAst(alloc, output); +} From 203b38fdac0b18d0e26418390dcaca914357c28c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Jan 2024 22:22:29 -0800 Subject: [PATCH 2/5] rewrite generate_help for personal style - Output to stdin instead of a file - Less nesting - Utilize ranged for loops instead of while loops - Eliminate unnecessary state tracking - Put help in a struct --- build.zig | 68 ++++++++------ src/cli/action.zig | 15 ++++ src/generate_help_strings.zig | 165 ---------------------------------- 3 files changed, 55 insertions(+), 193 deletions(-) delete mode 100644 src/generate_help_strings.zig diff --git a/build.zig b/build.zig index 6505f8aec..a08602a10 100644 --- a/build.zig +++ b/build.zig @@ -114,6 +114,12 @@ pub fn build(b: *std.Build) !void { "Build and install the benchmark executables.", ) orelse false; + const emit_helpgen = b.option( + bool, + "emit-helpgen", + "Build and install the helpgen executable.", + ) orelse false; + // On NixOS, the built binary from `zig build` needs to patch the rpath // into the built binary for it to be portable across the NixOS system // it was built for. We default this to true if we can detect we're in @@ -177,6 +183,10 @@ pub fn build(b: *std.Build) !void { // We can use wasmtime to test wasm b.enable_wasmtime = true; + // Help exe. This must be run before any dependent executables because + // otherwise the build will be cached without emit. That's clunky but meh. + if (emit_helpgen) addHelp(b, null); + // Add our benchmarks try benchSteps(b, target, optimize, config, emit_bench); @@ -197,8 +207,6 @@ pub fn build(b: *std.Build) !void { .{version}, )); - createHelp(b); - // Exe if (exe_) |exe| { exe.root_module.addOptions("build_options", exe_options); @@ -477,7 +485,10 @@ pub fn build(b: *std.Build) !void { } // On Mac we can build the embedding library. This only handles the macOS lib. - if (builtin.target.isDarwin() and target.result.os.tag == .macos) { + if (builtin.target.isDarwin() and + target.result.os.tag == .macos and + config.app_runtime == .none) + { // Create the universal macOS lib. const macos_lib_step, const macos_lib_path = try createMacOSLib( b, @@ -1100,42 +1111,43 @@ fn addDeps( } } - addHelp(step); + addHelp(b, step); return static_libs; } -var generate_help_step: *std.Build.Step.Run = undefined; -var help_strings: std.Build.LazyPath = undefined; - /// Generate help files -fn createHelp(b: *std.Build) void { - const generate_help = b.addExecutable(.{ - .name = "generate_help", - .root_source_file = .{ .path = "src/generate_help_strings.zig" }, - .target = b.host, - }); +fn addHelp( + b: *std.Build, + step_: ?*std.Build.Step.Compile, +) void { + // Our static state between runs. We memoize our help strings + // so that we only execute the help generation once. + const HelpState = struct { + var generated: ?std.Build.LazyPath = null; + }; - generate_help_step = b.addRunArtifact(generate_help); - generate_help_step.step.dependOn(&generate_help.step); + const help_output = HelpState.generated orelse strings: { + const help_exe = b.addExecutable(.{ + .name = "helpgen", + .root_source_file = .{ .path = "src/helpgen.zig" }, + .target = b.host, + }); + if (step_ == null) b.installArtifact(help_exe); - help_strings = generate_help_step.addOutputFileArg("help_strings.zig"); + const help_run = b.addRunArtifact(help_exe); + HelpState.generated = help_run.captureStdOut(); + break :strings HelpState.generated.?; + }; - if (builtin.target.isDarwin()) { - const generated = b.option([]const u8, "help_strings", "generated help file") orelse "help_strings"; - const write_file = b.addWriteFiles(); - help_strings = write_file.addCopyFile(help_strings, generated); + if (step_) |step| { + help_output.addStepDependencies(&step.step); + step.root_module.addAnonymousImport("help_strings", .{ + .root_source_file = help_output, + }); } } -/// Add the generated help files to the build. -fn addHelp(step: *std.Build.Step.Compile) void { - step.step.dependOn(&generate_help_step.step); - step.root_module.addAnonymousImport("help_strings", .{ - .root_source_file = help_strings, - }); -} - fn benchSteps( b: *std.Build, target: std.Build.ResolvedTarget, diff --git a/src/cli/action.zig b/src/cli/action.zig index f97a5d2cf..2a7e0c6a9 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -69,6 +69,21 @@ pub const Action = enum { .@"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/generate_help_strings.zig b/src/generate_help_strings.zig deleted file mode 100644 index 1b4ed35ca..000000000 --- a/src/generate_help_strings.zig +++ /dev/null @@ -1,165 +0,0 @@ -const std = @import("std"); -const ziglyph = @import("ziglyph"); -const Action = @import("cli/action.zig").Action; -const Config = @import("config/Config.zig"); - -pub fn searchConfigAst(alloc: std.mem.Allocator, output: std.fs.File) !void { - var ast = try std.zig.Ast.parse(alloc, @embedFile("config/Config.zig"), .zig); - defer ast.deinit(alloc); - - const config: Config = .{}; - - const tokens = ast.tokens.items(.tag); - - var set = std.StringHashMap(bool).init(alloc); - defer set.deinit(); - - try output.writeAll( - \\//THIS FILE IS AUTO GENERATED - \\//DO NOT MAKE ANY CHANGES TO THIS FILE! - ); - - try output.writeAll("\n\n"); - - inline for (@typeInfo(@TypeOf(config)).Struct.fields) |field| { - if (field.name[0] != '_') try set.put(field.name, false); - } - - var index: u32 = 0; - while (true) : (index += 1) { - if (index >= tokens.len) break; - const token = tokens[index]; - - if (token == .identifier) { - const slice = ast.tokenSlice(index); - // We need this check because the ast grabs the identifier with @"" in case it's used. - const key = if (slice[0] == '@') slice[2 .. slice.len - 1] else slice; - - if (key[0] == '_') continue; - - if (set.get(key)) |value| { - if (value) continue; - if (tokens[index - 1] != .doc_comment) continue; - - const comment = try consumeDocComments(alloc, ast, index - 1, &tokens); - const prop_type = ": " ++ "[:0]const u8 " ++ "= " ++ "\n"; - - try output.writeAll(slice); - try output.writeAll(prop_type); - // const concat = try std.mem.concat(self.alloc, u8, &.{ slice, prop_type }); - // try output.writeAll(concat); - try output.writeAll(comment); - try output.writeAll("\n\n"); - - try set.put(key, true); - } - } - if (token == .eof) break; - } -} - -fn actionPath(comptime action: Action) []const u8 { - return switch (action) { - .version => "cli/version.zig", - .@"list-fonts" => "cli/list_fonts.zig", - .@"list-keybinds" => "cli/list_keybinds.zig", - .@"list-themes" => "cli/list_themes.zig", - .@"list-colors" => "cli/list_colors.zig", - }; -} - -pub fn searchActionsAst(alloc: std.mem.Allocator, output: std.fs.File) !void { - inline for (@typeInfo(Action).Enum.fields) |field| { - const action = comptime std.meta.stringToEnum(Action, field.name).?; - - var ast = try std.zig.Ast.parse(alloc, @embedFile(comptime actionPath(action)), .zig); - const tokens = ast.tokens.items(.tag); - - var index: u32 = 0; - while (true) : (index += 1) { - if (tokens[index] == .keyword_fn) { - if (std.mem.eql(u8, ast.tokenSlice(index + 1), "run")) { - if (tokens[index - 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 consumeDocComments(alloc, ast, index - 2, &tokens); - const prop_type = "@\"+" ++ field.name ++ "\"" ++ ": " ++ "[:0]const u8 " ++ "= " ++ "\n"; - - try output.writeAll(prop_type); - try output.writeAll(comment); - try output.writeAll("\n\n"); - break; - } - } - } - } -} - -fn consumeDocComments(alloc: std.mem.Allocator, ast: std.zig.Ast, index: std.zig.Ast.TokenIndex, toks: anytype) ![]const u8 { - var lines = std.ArrayList([]const u8).init(alloc); - defer lines.deinit(); - - const tokens = toks.*; - var current_idx = index; - - // We iterate backwards because the doc_comment tokens should be on top of each other in case there are any. - while (true) : (current_idx -= 1) { - const token = tokens[current_idx]; - - if (token != .doc_comment) break; - // Insert at 0 so that we don't have the text in reverse. - try lines.insert(0, ast.tokenSlice(current_idx)[3..]); - } - - const prefix = findCommonPrefix(lines); - - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); - - 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; -} - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var arena = std.heap.ArenaAllocator.init(gpa.allocator()); - const alloc = arena.allocator(); - - const args = try std.process.argsAlloc(alloc); - defer std.process.argsFree(alloc, args); - - if (args.len != 2) { - std.debug.print("invalid number of arguments provided!", .{}); - std.process.exit(1); - } - - const path = args[1]; - - var output = try std.fs.cwd().createFile(path, .{}); - defer output.close(); - - try searchConfigAst(alloc, output); - try searchActionsAst(alloc, output); -} From b438998fb82d07e7f29a71cec30e1f46a7a7a0fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 09:29:26 -0800 Subject: [PATCH 3/5] cli: support --help and -h for actions --- src/cli/action.zig | 34 +++++++++++++++++++++++++++++++++- src/cli/args.zig | 11 +++++++++++ src/cli/list_colors.zig | 7 +++++++ src/cli/list_fonts.zig | 7 +++++++ src/cli/list_keybinds.zig | 7 +++++++ src/cli/list_themes.zig | 7 +++++++ src/cli/version.zig | 26 +++++++++++++++++++++++++- 7 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/cli/action.zig b/src/cli/action.zig index 2a7e0c6a9..a0fa216eb 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -1,5 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const help_strings = @import("help_strings"); const list_fonts = @import("list_fonts.zig"); const version = @import("version.zig"); @@ -35,6 +36,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); @@ -61,8 +65,36 @@ pub const Action = enum { /// 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), .@"list-fonts" => try list_fonts.run(alloc), .@"list-keybinds" => try list_keybinds.run(alloc), .@"list-themes" => try list_themes.run(alloc), 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/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 dca20ad2f..fb12faa2e 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -1,12 +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() !u8 { +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.print("Ghostty {s}\n\n", .{build_config.version_string}); try stdout.print("Build Config\n", .{}); From 1778905f530cb9dc4ab473ad18213c027448dd93 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 09:49:17 -0800 Subject: [PATCH 4/5] cli: add help command --- src/cli/action.zig | 20 +++++++++++++ src/cli/help.zig | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/cli/help.zig diff --git a/src/cli/action.zig b/src/cli/action.zig index a0fa216eb..2e3afb1c0 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -3,6 +3,7 @@ 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"); @@ -15,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", @@ -48,18 +52,33 @@ 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; } @@ -95,6 +114,7 @@ pub const Action = enum { 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), 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 `--=` 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 ` 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 `+ --help` to see the help for a specific action, + \\where `` is one of actions listed below. + \\ + ); + + return 0; +} From 7595b9b4bf75a243940f64da957e5c8a8457f191 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 09:49:27 -0800 Subject: [PATCH 5/5] re-add helpgen --- src/helpgen.zig | 167 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/helpgen.zig 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; +}