From dbfedd240056001bf86e1b1121ab18dcc9fdc62a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 15 Sep 2023 09:17:58 -0700 Subject: [PATCH 1/5] Add --version for outputting version, framework for more actions --- src/cli_action.zig | 107 +++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 35 +++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/cli_action.zig diff --git a/src/cli_action.zig b/src/cli_action.zig new file mode 100644 index 000000000..095770b56 --- /dev/null +++ b/src/cli_action.zig @@ -0,0 +1,107 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const build_config = @import("build_config.zig"); + +/// Special commands that can be invoked via CLI flags. These are all +/// invoked by using `+` 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, + + 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, + }; + + /// 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: ?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; + + // 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; + } + + return null; + } + + /// Run the action. This returns the exit code to exit with. + pub fn run(self: Action, alloc: Allocator) !u8 { + _ = alloc; + return switch (self) { + .version => try runVersion(), + }; + } +}; + +fn runVersion() !u8 { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Ghostty {s}\n", .{build_config.version_string}); + return 0; +} + +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); + } +} diff --git a/src/main.zig b/src/main.zig index 03603392d..da7eb665b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,10 +1,12 @@ const std = @import("std"); const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); const options = @import("build_options"); const glfw = @import("glfw"); const macos = @import("macos"); const tracy = @import("tracy"); +const cli_action = @import("cli_action.zig"); const internal_os = @import("os/main.zig"); const xev = @import("xev"); const fontconfig = @import("fontconfig"); @@ -32,6 +34,38 @@ pub fn main() !void { defer state.deinit(); const alloc = state.alloc; + // Before we do anything else, we need to check for special commands + // via the CLI flags. + if (cli_action.Action.detectCLI(alloc)) |action_| { + if (action_) |action| { + std.log.info("executing CLI action={}", .{action}); + std.os.exit(action.run(alloc) catch |err| err: { + std.log.err("CLI action failed error={}", .{err}); + break :err 1; + }); + return; + } + } else |err| { + const stderr = std.io.getStdErr().writer(); + defer std.os.exit(1); + const ErrSet = @TypeOf(err) || error{Unknown}; + switch (@as(ErrSet, @errSetCast(err))) { + error.MultipleActions => try stderr.print( + "Error: multiple CLI actions specified. You must specify only one\n" ++ + "action starting with the `+` character.\n", + .{}, + ), + + error.InvalidAction => try stderr.print( + "Error: unknown CLI action specified. CLI actions are specified with\n" ++ + "the '+' character.\n", + .{}, + ), + + else => try stderr.print("invalid CLI invocation err={}\n", .{err}), + } + } + // Create our app state var app = try App.create(alloc); defer app.destroy(); @@ -187,6 +221,7 @@ test { _ = @import("renderer.zig"); _ = @import("termio.zig"); _ = @import("input.zig"); + _ = @import("cli_action.zig"); // Libraries _ = @import("segmented_pool.zig"); From 26313bc85df85d027fa813a0252d0b7f2f1d374d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 15 Sep 2023 09:27:48 -0700 Subject: [PATCH 2/5] do not write logs to stderr for cli actions --- src/main.zig | 95 ++++++++++++++++++++++++++++---------------------- src/main_c.zig | 5 ++- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/main.zig b/src/main.zig index da7eb665b..3716d698e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -24,28 +24,11 @@ const Ghostty = @import("main_c.zig").Ghostty; pub var state: GlobalState = undefined; pub fn main() !void { - if (comptime builtin.mode == .Debug) { - std.log.warn("This is a debug build. Performance will be very poor.", .{}); - std.log.warn("You should only use a debug build for developing Ghostty.", .{}); - std.log.warn("Otherwise, please rebuild in a release mode.", .{}); - } - - state.init(); - defer state.deinit(); - const alloc = state.alloc; - - // Before we do anything else, we need to check for special commands - // via the CLI flags. - if (cli_action.Action.detectCLI(alloc)) |action_| { - if (action_) |action| { - std.log.info("executing CLI action={}", .{action}); - std.os.exit(action.run(alloc) catch |err| err: { - std.log.err("CLI action failed error={}", .{err}); - break :err 1; - }); - return; - } - } else |err| { + // We first start by initializing our global state. This will setup + // process-level state we need to run the terminal. The reason we use + // a global is because the C API needs to be able to access this state; + // no other Zig code should EVER access the global state. + state.init() catch |err| { const stderr = std.io.getStdErr().writer(); defer std.os.exit(1); const ErrSet = @TypeOf(err) || error{Unknown}; @@ -64,6 +47,24 @@ pub fn main() !void { else => try stderr.print("invalid CLI invocation err={}\n", .{err}), } + }; + defer state.deinit(); + const alloc = state.alloc; + + if (comptime builtin.mode == .Debug) { + std.log.warn("This is a debug build. Performance will be very poor.", .{}); + std.log.warn("You should only use a debug build for developing Ghostty.", .{}); + std.log.warn("Otherwise, please rebuild in a release mode.", .{}); + } + + // Execute our action if we have one + if (state.action) |action| { + std.log.info("executing CLI action={}", .{action}); + std.os.exit(action.run(alloc) catch |err| err: { + std.log.err("CLI action failed error={}", .{err}); + break :err 1; + }); + return; } // Create our app state @@ -125,6 +126,11 @@ pub const std_options = struct { logger.log(std.heap.c_allocator, mac_level, format, args); } + // If we have an action executing, we don't log to stderr. + // TODO(mitchellh): flag to enable logs + // TODO(mitchellh): flag to send logs to file + if (state.action != null) return; + // Always try default to send to stderr const stderr = std.io.getStdErr().writer(); nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; @@ -140,31 +146,17 @@ pub const GlobalState = struct { gpa: ?GPA, alloc: std.mem.Allocator, tracy: if (tracy.enabled) ?tracy.Allocator(null) else void, + action: ?cli_action.Action, - pub fn init(self: *GlobalState) void { - // Output some debug information right away - std.log.info("ghostty version={s}", .{build_config.version_string}); - std.log.info("runtime={}", .{build_config.app_runtime}); - std.log.info("font_backend={}", .{build_config.font_backend}); - std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); - if (comptime build_config.font_backend.hasFontconfig()) { - std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); - } - std.log.info("renderer={}", .{renderer.Renderer}); - std.log.info("libxev backend={}", .{xev.backend}); - - // First things first, we fix our file descriptors - internal_os.fixMaxFiles(); - - // We need to make sure the process locale is set properly. Locale - // affects a lot of behaviors in a shell. - internal_os.ensureLocale(); - + pub fn init(self: *GlobalState) !void { // Initialize ourself to nothing so we don't have any extra state. + // IMPORTANT: this MUST be initialized before any log output because + // the log function uses the global state. self.* = .{ .gpa = null, .alloc = undefined, .tracy = undefined, + .action = null, }; errdefer self.deinit(); @@ -198,6 +190,27 @@ pub const GlobalState = struct { self.tracy = tracy.allocator(base, null); break :alloc self.tracy.?.allocator(); }; + + // We first try to parse any action that we may be executing. + self.action = try cli_action.Action.detectCLI(self.alloc); + + // Output some debug information right away + std.log.info("ghostty version={s}", .{build_config.version_string}); + std.log.info("runtime={}", .{build_config.app_runtime}); + std.log.info("font_backend={}", .{build_config.font_backend}); + std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); + if (comptime build_config.font_backend.hasFontconfig()) { + std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); + } + std.log.info("renderer={}", .{renderer.Renderer}); + std.log.info("libxev backend={}", .{xev.backend}); + + // First things first, we fix our file descriptors + internal_os.fixMaxFiles(); + + // We need to make sure the process locale is set properly. Locale + // affects a lot of behaviors in a shell. + internal_os.ensureLocale(); } /// Cleans up the global state. This doesn't _need_ to be called but diff --git a/src/main_c.zig b/src/main_c.zig index 05cf43897..c68cdbf36 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -27,6 +27,9 @@ pub usingnamespace apprt.runtime.CAPI; /// one global state but it has zero practical benefit. export fn ghostty_init() c_int { assert(builtin.link_libc); - main.state.init(); + main.state.init() catch |err| { + std.log.err("failed to initialize ghostty error={}", .{err}); + return 1; + }; return 0; } From bcd88619c6d34c8c3e3899421565dfd1d6386078 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 15 Sep 2023 12:15:12 -0700 Subject: [PATCH 3/5] can enable logging for CLI actions with GHOSTTY_LOG env var --- src/main.zig | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/main.zig b/src/main.zig index 3716d698e..936c167d7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -126,14 +126,15 @@ pub const std_options = struct { logger.log(std.heap.c_allocator, mac_level, format, args); } - // If we have an action executing, we don't log to stderr. - // TODO(mitchellh): flag to enable logs - // TODO(mitchellh): flag to send logs to file - if (state.action != null) return; + switch (state.logging) { + .disabled => {}, - // Always try default to send to stderr - const stderr = std.io.getStdErr().writer(); - nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; + .stderr => { + // Always try default to send to stderr + const stderr = std.io.getStdErr().writer(); + nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; + }, + } } }; @@ -147,6 +148,13 @@ pub const GlobalState = struct { alloc: std.mem.Allocator, tracy: if (tracy.enabled) ?tracy.Allocator(null) else void, action: ?cli_action.Action, + logging: Logging, + + /// Where logging should go + pub const Logging = union(enum) { + disabled: void, + stderr: void, + }; pub fn init(self: *GlobalState) !void { // Initialize ourself to nothing so we don't have any extra state. @@ -157,6 +165,7 @@ pub const GlobalState = struct { .alloc = undefined, .tracy = undefined, .action = null, + .logging = .{ .stderr = {} }, }; errdefer self.deinit(); @@ -194,6 +203,22 @@ pub const GlobalState = struct { // We first try to parse any action that we may be executing. self.action = try cli_action.Action.detectCLI(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 + // output. + if (self.action != null) self.logging = .{ .disabled = {} }; + + // I don't love the env var name but I don't have it in my heart + // to parse CLI args 3 times (once for actions, once for config, + // maybe once for logging) so for now this is an easy way to do + // this. Env vars are useful for logging too because they are + // easy to set. + if (std.os.getenv("GHOSTTY_LOG")) |v| { + if (v.len > 0) { + self.logging = .{ .stderr = {} }; + } + } + // Output some debug information right away std.log.info("ghostty version={s}", .{build_config.version_string}); std.log.info("runtime={}", .{build_config.app_runtime}); From e11299a7752b7f7a5526d0e8bd76766b34481c73 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 15 Sep 2023 12:17:59 -0700 Subject: [PATCH 4/5] cli actions can be "+" --- src/cli_action.zig | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/cli_action.zig b/src/cli_action.zig index 095770b56..d852c243f 100644 --- a/src/cli_action.zig +++ b/src/cli_action.zig @@ -40,7 +40,7 @@ pub const Action = enum { pending = std.meta.stringToEnum(Action, arg[1..]) orelse return Error.InvalidAction; } - return null; + return pending; } /// Run the action. This returns the exit code to exit with. @@ -105,3 +105,38 @@ test "parse action version" { 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); + } +} From 07481fe703ab8d82cc3ae6658f120d2c14851688 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 15 Sep 2023 12:22:35 -0700 Subject: [PATCH 5/5] output more information for version --- src/cli_action.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli_action.zig b/src/cli_action.zig index d852c243f..e62b3bfd5 100644 --- a/src/cli_action.zig +++ b/src/cli_action.zig @@ -1,7 +1,9 @@ const std = @import("std"); const builtin = @import("builtin"); +const xev = @import("xev"); const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); +const renderer = @import("renderer.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -54,7 +56,13 @@ pub const Action = enum { fn runVersion() !u8 { const stdout = std.io.getStdOut().writer(); - try stdout.print("Ghostty {s}\n", .{build_config.version_string}); + try stdout.print("Ghostty {s}\n\n", .{build_config.version_string}); + try stdout.print("Build Config\n", .{}); + try stdout.print(" - build mode : {}\n", .{builtin.mode}); + try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime}); + try stdout.print(" - font engine: {}\n", .{build_config.font_backend}); + try stdout.print(" - renderer : {}\n", .{renderer.Renderer}); + try stdout.print(" - libxev : {}\n", .{xev.backend}); return 0; }