diff --git a/include/ghostty.h b/include/ghostty.h
index 181f7b7f8..73c708c6b 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -778,8 +778,8 @@ typedef struct {
//-------------------------------------------------------------------
// Published API
-int ghostty_init(void);
-void ghostty_cli_main(uintptr_t, char**);
+int ghostty_init(uintptr_t, char**);
+void ghostty_cli_try_action(void);
ghostty_info_s ghostty_info(void);
const char* ghostty_translate(const char*);
diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist
index dcce61373..ff391c0f8 100644
--- a/macos/Ghostty-Info.plist
+++ b/macos/Ghostty-Info.plist
@@ -48,8 +48,8 @@
LSEnvironment
- GHOSTTY_MAC_APP
- 1
+ GHOSTTY_MAC_LAUNCH_SOURCE
+ app
MDItemKeywords
Terminal
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index cf806c7bd..08c3ef3b3 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -13,6 +13,7 @@
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
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 */; };
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.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 = ""; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; };
+ A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = ""; };
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; };
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; };
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; };
@@ -516,6 +518,7 @@
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
+ A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */,
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
A51194122E05D003007258CC /* Optional+Extension.swift */,
@@ -799,6 +802,7 @@
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */,
+ A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift
index 990ef8ef1..ad32f4e70 100644
--- a/macos/Sources/App/macOS/main.swift
+++ b/macos/Sources/App/macOS/main.swift
@@ -2,13 +2,32 @@ import AppKit
import Cocoa
import GhosttyKit
-// We put the GHOSTTY_MAC_APP env var into the Info.plist to detect
-// whether we launch from the app or not. A user can fake this if
-// they want but they're doing so at their own detriment...
-let process = ProcessInfo.processInfo
-if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") {
- ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv)
- exit(1)
+// Initialize Ghostty global state. We do this once right away because the
+// CLI APIs require it and it lets us ensure it is done immediately for the
+// rest of the app.
+if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS {
+ Ghostty.logger.critical("ghostty_init failed")
+
+ // 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)
+ }
}
+// 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)
diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift
index ba0b95212..17abe2b0e 100644
--- a/macos/Sources/Ghostty/Ghostty.App.swift
+++ b/macos/Sources/Ghostty/Ghostty.App.swift
@@ -45,12 +45,6 @@ extension Ghostty {
}
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.
self.config = Config()
if self.config.config == nil {
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index e96f555d3..f30f2f6f9 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -61,9 +61,12 @@ extension Ghostty {
/// its up to the env var being set in the correct circumstance.
static var launchSource: LaunchSource {
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
}
}
diff --git a/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift b/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift
new file mode 100644
index 000000000..b6df4a60f
--- /dev/null
+++ b/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift
@@ -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)
+ }
+}
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 0121494b7..30a2d9ff6 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -884,7 +884,7 @@ pub const Surface = struct {
}
// 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
// remove the LANGUAGE env var so that we don't inherit
diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig
index 052c9f3e4..7fa2d2f95 100644
--- a/src/build/GhosttyXcodebuild.zig
+++ b/src/build/GhosttyXcodebuild.zig
@@ -122,10 +122,6 @@ pub fn init(
if (b.args) |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;
diff --git a/src/main_c.zig b/src/main_c.zig
index 1b73d7327..0722900e7 100644
--- a/src/main_c.zig
+++ b/src/main_c.zig
@@ -46,17 +46,11 @@ const Info = extern struct {
};
};
-/// Initialize ghostty global state. It is possible to have more than
-/// one global state but it has zero practical benefit.
-export fn ghostty_init() c_int {
+/// Initialize ghostty global state.
+export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
assert(builtin.link_libc);
- // Since in the lib we don't go through start.zig, we need
- // to populate argv so that inspecting std.os.argv doesn't
- // touch uninitialized memory.
- var argv: [0][*:0]u8 = .{};
- std.os.argv = &argv;
-
+ std.os.argv = argv[0..argc];
state.init() catch |err| {
std.log.err("failed to initialize ghostty error={}", .{err});
return 1;
@@ -65,15 +59,17 @@ export fn ghostty_init() c_int {
return 0;
}
-/// This is the entrypoint for the CLI version of Ghostty. This
-/// is mutually exclusive to ghostty_init. Do NOT run ghostty_init
-/// if you are going to run this. This will not return.
-export fn ghostty_cli_main(argc: usize, argv: [*][*:0]u8) noreturn {
- std.os.argv = argv[0..argc];
- main.main() catch |err| {
- std.log.err("failed to run ghostty error={}", .{err});
+/// Runs an action if it is specified. If there is no action this returns
+/// false. If there is an action then this doesn't return.
+export fn ghostty_cli_try_action() void {
+ const action = state.action orelse return;
+ std.log.info("executing CLI action={}", .{action});
+ posix.exit(action.run(state.alloc) catch |err| {
+ std.log.err("CLI action failed error={}", .{err});
posix.exit(1);
- };
+ });
+
+ posix.exit(0);
}
/// Return metadata about Ghostty, such as version, build mode, etc.
diff --git a/src/os/desktop.zig b/src/os/desktop.zig
index 3bc843e5c..93bfb74bc 100644
--- a/src/os/desktop.zig
+++ b/src/os/desktop.zig
@@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool {
// 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
// was launched from the desktop.
- if (build_config.artifact == .lib and
- posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true;
+ if (build_config.artifact == .lib) lib: {
+ 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;
},