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; },