macos: support configuration via CLI arguments

This makes it so `zig build run` can take arguments such as
`--config-default-files=false` or any other configuration. Previously,
it only accepted commands such as `+version`.

Incidentally, this also makes it so that the app in general can now take
configuration arguments via the CLI if it is launched as a new instance
via `open`. For example:

    open -n Ghostty.app --args --config-default-files=false

This previously didn't work. This is kind of cool.

To make this work, the libghostty C API was modified so that
initialization requires the CLI args, and there is a new C API to try to
execute an action if it was set.
This commit is contained in:
Mitchell Hashimoto
2025-07-05 21:04:59 -07:00
parent 82cad3cf33
commit 984d123fe4
11 changed files with 70 additions and 42 deletions

View File

@ -778,8 +778,8 @@ typedef struct {
//------------------------------------------------------------------- //-------------------------------------------------------------------
// Published API // Published API
int ghostty_init(void); int ghostty_init(uintptr_t, char**);
void ghostty_cli_main(uintptr_t, char**); void ghostty_cli_try_action(void);
ghostty_info_s ghostty_info(void); ghostty_info_s ghostty_info(void);
const char* ghostty_translate(const char*); const char* ghostty_translate(const char*);

View File

@ -48,8 +48,8 @@
<string></string> <string></string>
<key>LSEnvironment</key> <key>LSEnvironment</key>
<dict> <dict>
<key>GHOSTTY_MAC_APP</key> <key>GHOSTTY_MAC_LAUNCH_SOURCE</key>
<string>1</string> <string>app</string>
</dict> </dict>
<key>MDItemKeywords</key> <key>MDItemKeywords</key>
<string>Terminal</string> <string>Terminal</string>

View File

@ -13,6 +13,7 @@
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */; };
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; }; A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; }; A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; };
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; }; A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
@ -158,6 +159,7 @@
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = "<group>"; };
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; }; A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; };
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; }; A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; };
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; }; A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; };
@ -516,6 +518,7 @@
A586366A2DF0A98900E04A10 /* Array+Extension.swift */, A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
A50297342DFA0F3300B4E924 /* Double+Extension.swift */, A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */,
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
A51194122E05D003007258CC /* Optional+Extension.swift */, A51194122E05D003007258CC /* Optional+Extension.swift */,
@ -799,6 +802,7 @@
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */, A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */,
A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */, CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */, A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,

View File

@ -2,13 +2,32 @@ import AppKit
import Cocoa import Cocoa
import GhosttyKit import GhosttyKit
// We put the GHOSTTY_MAC_APP env var into the Info.plist to detect // Initialize Ghostty global state. We do this once right away because the
// whether we launch from the app or not. A user can fake this if // CLI APIs require it and it lets us ensure it is done immediately for the
// they want but they're doing so at their own detriment... // rest of the app.
let process = ProcessInfo.processInfo if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS {
if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") { Ghostty.logger.critical("ghostty_init failed")
ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv)
// We also write to stderr if this is executed from the CLI or zig run
switch Ghostty.launchSource {
case .cli, .zig_run:
let stderrHandle = FileHandle.standardError
stderrHandle.write(
"Ghostty failed to initialize! If you're executing Ghostty from the command line\n" +
"then this is usually because an invalid action or multiple actions were specified.\n" +
"Actions start with the `+` character.\n\n" +
"View all available actions by running `ghostty +help`.\n")
exit(1)
case .app:
// For the app we exit immediately. We should handle this case more
// gracefully in the future.
exit(1) exit(1)
} }
}
// This will run the CLI action and exit if one was specified. A CLI
// action is a command starting with a `+`, such as `ghostty +boo`.
ghostty_cli_try_action();
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

View File

@ -45,12 +45,6 @@ extension Ghostty {
} }
init() { init() {
// Initialize ghostty global state. This happens once per process.
if ghostty_init() != GHOSTTY_SUCCESS {
logger.critical("ghostty_init failed, weird things may happen")
readiness = .error
}
// Initialize the global configuration. // Initialize the global configuration.
self.config = Config() self.config = Config()
if self.config.config == nil { if self.config.config == nil {

View File

@ -61,9 +61,12 @@ extension Ghostty {
/// its up to the env var being set in the correct circumstance. /// its up to the env var being set in the correct circumstance.
static var launchSource: LaunchSource { static var launchSource: LaunchSource {
guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else { guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else {
return .app // We default to the CLI because the app bundle always sets the
// source. If its unset we assume we're in a CLI environment.
return .cli
} }
// If the env var is set but its unknown then we default back to the app.
return LaunchSource(rawValue: envValue) ?? .app return LaunchSource(rawValue: envValue) ?? .app
} }
} }

View File

@ -0,0 +1,9 @@
import Foundation
extension FileHandle: @retroactive TextOutputStream {
/// Write a string to a filehandle.
public func write(_ string: String) {
let data = Data(string.utf8)
self.write(data)
}
}

View File

@ -884,7 +884,7 @@ pub const Surface = struct {
} }
// Remove this so that running `ghostty` within Ghostty works. // Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_APP"); env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
// If we were launched from the desktop then we want to // If we were launched from the desktop then we want to
// remove the LANGUAGE env var so that we don't inherit // remove the LANGUAGE env var so that we don't inherit

View File

@ -122,10 +122,6 @@ pub fn init(
if (b.args) |args| { if (b.args) |args| {
open.addArgs(args); open.addArgs(args);
} else {
// This tricks the app into thinking it's running from the
// app bundle so we don't execute our CLI mode.
open.setEnvironmentVariable("GHOSTTY_MAC_APP", "1");
} }
break :open open; break :open open;

View File

@ -46,17 +46,11 @@ const Info = extern struct {
}; };
}; };
/// Initialize ghostty global state. It is possible to have more than /// Initialize ghostty global state.
/// one global state but it has zero practical benefit. export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
export fn ghostty_init() c_int {
assert(builtin.link_libc); assert(builtin.link_libc);
// Since in the lib we don't go through start.zig, we need std.os.argv = argv[0..argc];
// to populate argv so that inspecting std.os.argv doesn't
// touch uninitialized memory.
var argv: [0][*:0]u8 = .{};
std.os.argv = &argv;
state.init() catch |err| { state.init() catch |err| {
std.log.err("failed to initialize ghostty error={}", .{err}); std.log.err("failed to initialize ghostty error={}", .{err});
return 1; return 1;
@ -65,15 +59,17 @@ export fn ghostty_init() c_int {
return 0; return 0;
} }
/// This is the entrypoint for the CLI version of Ghostty. This /// Runs an action if it is specified. If there is no action this returns
/// is mutually exclusive to ghostty_init. Do NOT run ghostty_init /// false. If there is an action then this doesn't return.
/// if you are going to run this. This will not return. export fn ghostty_cli_try_action() void {
export fn ghostty_cli_main(argc: usize, argv: [*][*:0]u8) noreturn { const action = state.action orelse return;
std.os.argv = argv[0..argc]; std.log.info("executing CLI action={}", .{action});
main.main() catch |err| { posix.exit(action.run(state.alloc) catch |err| {
std.log.err("failed to run ghostty error={}", .{err}); std.log.err("CLI action failed error={}", .{err});
posix.exit(1); posix.exit(1);
}; });
posix.exit(0);
} }
/// Return metadata about Ghostty, such as version, build mode, etc. /// Return metadata about Ghostty, such as version, build mode, etc.

View File

@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool {
// This special case is so that if we launch the app via the // This special case is so that if we launch the app via the
// app bundle (i.e. via open) then we still treat it as if it // app bundle (i.e. via open) then we still treat it as if it
// was launched from the desktop. // was launched from the desktop.
if (build_config.artifact == .lib and if (build_config.artifact == .lib) lib: {
posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true; const env = "GHOSTTY_MAC_LAUNCH_SOURCE";
const source = posix.getenv(env) orelse break :lib;
// Source can be "app", "cli", or "zig_run". We assume
// its the desktop only if its "app". We may want to do
// "zig_run" but at the moment there's no reason.
if (std.mem.eql(u8, source, "app")) break :macos true;
}
break :macos c.getppid() == 1; break :macos c.getppid() == 1;
}, },