mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #470 from mitchellh/cli-actions
`--version` and general framework for CLI subcommands
This commit is contained in:
150
src/cli_action.zig
Normal file
150
src/cli_action.zig
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
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 `+<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,
|
||||||
|
|
||||||
|
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 pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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\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;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse action none" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"--a=42 --b --b-f=false",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse action version" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"--a=42 --b --b-f=false --version",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action.? == .version);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"--version --a=42 --b --b-f=false",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action.? == .version);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"--c=84 --d --version --a=42 --b --b-f=false",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action.? == .version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse action plus" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"--a=42 --b --b-f=false +version",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action.? == .version);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"+version --a=42 --b --b-f=false",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action.? == .version);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
alloc,
|
||||||
|
"--c=84 --d +version --a=42 --b --b-f=false",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
const action = try Action.detectIter(&iter);
|
||||||
|
try testing.expect(action.? == .version);
|
||||||
|
}
|
||||||
|
}
|
121
src/main.zig
121
src/main.zig
@ -1,10 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
const build_config = @import("build_config.zig");
|
const build_config = @import("build_config.zig");
|
||||||
const options = @import("build_options");
|
const options = @import("build_options");
|
||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
|
const cli_action = @import("cli_action.zig");
|
||||||
const internal_os = @import("os/main.zig");
|
const internal_os = @import("os/main.zig");
|
||||||
const xev = @import("xev");
|
const xev = @import("xev");
|
||||||
const fontconfig = @import("fontconfig");
|
const fontconfig = @import("fontconfig");
|
||||||
@ -22,15 +24,48 @@ const Ghostty = @import("main_c.zig").Ghostty;
|
|||||||
pub var state: GlobalState = undefined;
|
pub var state: GlobalState = undefined;
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
|
// 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};
|
||||||
|
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}),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defer state.deinit();
|
||||||
|
const alloc = state.alloc;
|
||||||
|
|
||||||
if (comptime builtin.mode == .Debug) {
|
if (comptime builtin.mode == .Debug) {
|
||||||
std.log.warn("This is a debug build. Performance will be very poor.", .{});
|
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("You should only use a debug build for developing Ghostty.", .{});
|
||||||
std.log.warn("Otherwise, please rebuild in a release mode.", .{});
|
std.log.warn("Otherwise, please rebuild in a release mode.", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
state.init();
|
// Execute our action if we have one
|
||||||
defer state.deinit();
|
if (state.action) |action| {
|
||||||
const alloc = state.alloc;
|
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
|
// Create our app state
|
||||||
var app = try App.create(alloc);
|
var app = try App.create(alloc);
|
||||||
@ -91,9 +126,15 @@ pub const std_options = struct {
|
|||||||
logger.log(std.heap.c_allocator, mac_level, format, args);
|
logger.log(std.heap.c_allocator, mac_level, format, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always try default to send to stderr
|
switch (state.logging) {
|
||||||
const stderr = std.io.getStdErr().writer();
|
.disabled => {},
|
||||||
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;
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,31 +147,25 @@ pub const GlobalState = struct {
|
|||||||
gpa: ?GPA,
|
gpa: ?GPA,
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
tracy: if (tracy.enabled) ?tracy.Allocator(null) else void,
|
tracy: if (tracy.enabled) ?tracy.Allocator(null) else void,
|
||||||
|
action: ?cli_action.Action,
|
||||||
|
logging: Logging,
|
||||||
|
|
||||||
pub fn init(self: *GlobalState) void {
|
/// Where logging should go
|
||||||
// Output some debug information right away
|
pub const Logging = union(enum) {
|
||||||
std.log.info("ghostty version={s}", .{build_config.version_string});
|
disabled: void,
|
||||||
std.log.info("runtime={}", .{build_config.app_runtime});
|
stderr: void,
|
||||||
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.
|
// 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.* = .{
|
self.* = .{
|
||||||
.gpa = null,
|
.gpa = null,
|
||||||
.alloc = undefined,
|
.alloc = undefined,
|
||||||
.tracy = undefined,
|
.tracy = undefined,
|
||||||
|
.action = null,
|
||||||
|
.logging = .{ .stderr = {} },
|
||||||
};
|
};
|
||||||
errdefer self.deinit();
|
errdefer self.deinit();
|
||||||
|
|
||||||
@ -164,6 +199,43 @@ pub const GlobalState = struct {
|
|||||||
self.tracy = tracy.allocator(base, null);
|
self.tracy = tracy.allocator(base, null);
|
||||||
break :alloc self.tracy.?.allocator();
|
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);
|
||||||
|
|
||||||
|
// 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});
|
||||||
|
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
|
/// Cleans up the global state. This doesn't _need_ to be called but
|
||||||
@ -187,6 +259,7 @@ test {
|
|||||||
_ = @import("renderer.zig");
|
_ = @import("renderer.zig");
|
||||||
_ = @import("termio.zig");
|
_ = @import("termio.zig");
|
||||||
_ = @import("input.zig");
|
_ = @import("input.zig");
|
||||||
|
_ = @import("cli_action.zig");
|
||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
_ = @import("segmented_pool.zig");
|
_ = @import("segmented_pool.zig");
|
||||||
|
@ -27,6 +27,9 @@ pub usingnamespace apprt.runtime.CAPI;
|
|||||||
/// one global state but it has zero practical benefit.
|
/// one global state but it has zero practical benefit.
|
||||||
export fn ghostty_init() c_int {
|
export fn ghostty_init() c_int {
|
||||||
assert(builtin.link_libc);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user