From 423e55ce0faf6e350295aeb19d6406c11bd36c90 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Sep 2023 10:13:12 -0700 Subject: [PATCH 1/6] macos: change executale name to "ghostty" (lowercase) This way we can also put this on the PATH and use it as a CLI... --- macos/Ghostty-Info.plist | 5 +---- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 253b170f1..0c67376eb 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -1,8 +1,5 @@ - - NSMainNibFile - MainMenu - + diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index bf290f623..a2d86af81 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -424,11 +424,13 @@ DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + EXECUTABLE_NAME = ghostty; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainNibFile = MainMenu; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -457,12 +459,14 @@ DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + EXECUTABLE_NAME = ghostty; GCC_OPTIMIZATION_LEVEL = fast; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainNibFile = MainMenu; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", From cdbf16e13bbdc7e634891613f5c33c9d67d75475 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Sep 2023 11:10:46 -0700 Subject: [PATCH 2/6] macos: set env var for app bundle to detect app launch vs CLI --- macos/Ghostty-Info.plist | 7 ++++++- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++++ macos/Sources/AppDelegate.swift | 1 - macos/Sources/main.swift | 12 ++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/main.swift diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 0c67376eb..de2c98bf9 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -1,5 +1,10 @@ - + + GHOSTTY_MAC_APP + 1 + LSEnvironment + + diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a2d86af81..87e4ca7ed 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; }; A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; }; A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; + A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* PrimaryView.swift */; }; A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; }; /* End PBXBuildFile section */ @@ -65,6 +66,7 @@ A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = ""; }; A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; + A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = ""; }; A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -128,6 +130,7 @@ A54CD6ED299BEB14008C95BB /* Sources */ = { isa = PBXGroup; children = ( + A5FEB2FF2ABB69450068369E /* main.swift */, A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */, 857F63802A5E64F200CA4815 /* MainMenu.xib */, A53426362A7DC53000EBB7A2 /* Features */, @@ -279,6 +282,7 @@ A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */, + A5FEB3002ABB69450068369E /* main.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */, A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index e412de441..ebf728d83 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -2,7 +2,6 @@ import AppKit import OSLog import GhosttyKit -@NSApplicationMain class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyAppStateDelegate { // The application logger. We should probably move this at some point to a dedicated // class/struct but for now it lives here! 🤷‍♂️ diff --git a/macos/Sources/main.swift b/macos/Sources/main.swift new file mode 100644 index 000000000..ee38c8d55 --- /dev/null +++ b/macos/Sources/main.swift @@ -0,0 +1,12 @@ +import AppKit +import Cocoa + +// 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"] == "") { + AppDelegate.logger.warning("NOT IN THE MAC APP") +} + +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) From 7059b4f74d674e70590e65d7ad35921ea001521a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Sep 2023 12:35:52 -0700 Subject: [PATCH 3/6] apprt/embedded: ghostty_cli_main --- include/ghostty.h | 1 + macos/Sources/main.swift | 4 +++- src/main.zig | 40 +++++++++++++++++++++++++++++++--------- src/main_c.zig | 20 ++++++++++++++++++++ 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 1bdb4fc31..42e83db5f 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -351,6 +351,7 @@ typedef struct { // Published API int ghostty_init(void); +void ghostty_cli_main(uintptr_t, char **); ghostty_info_s ghostty_info(void); ghostty_config_t ghostty_config_new(); diff --git a/macos/Sources/main.swift b/macos/Sources/main.swift index ee38c8d55..9a462b440 100644 --- a/macos/Sources/main.swift +++ b/macos/Sources/main.swift @@ -1,12 +1,14 @@ 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"] == "") { - AppDelegate.logger.warning("NOT IN THE MAC APP") + ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv) + exit(1) } _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/src/main.zig b/src/main.zig index c61f94c5e..37e35e490 100644 --- a/src/main.zig +++ b/src/main.zig @@ -23,7 +23,13 @@ const Ghostty = @import("main_c.zig").Ghostty; /// rely on allocators being passed in as parameters. pub var state: GlobalState = undefined; -pub fn main() !void { +/// The return type for main() depends on the build artifact. +const MainReturn = switch (build_config.artifact) { + .lib => noreturn, + else => void, +}; + +pub fn main() !MainReturn { // 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; @@ -67,6 +73,24 @@ pub fn main() !void { return; } + if (comptime build_config.app_runtime == .none) { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Usage: ghostty + [flags]\n\n", .{}); + try stdout.print( + \\This is the Ghostty helper CLI that accompanies the graphical Ghostty app. + \\To launch the terminal directly, please launch the graphical app + \\(i.e. Ghostty.app on macOS). This CLI can be used to perform various + \\actions such as inspecting the version, listing fonts, etc. + \\ + \\We don't have proper help output yet, sorry! Please refer to the + \\source code or Discord community for help for now. We'll fix this in time. + , + .{}, + ); + + std.os.exit(0); + } + // Create our app state var app = try App.create(alloc); defer app.destroy(); @@ -156,6 +180,7 @@ pub const GlobalState = struct { stderr: void, }; + /// Initialize the global state. 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 @@ -201,15 +226,12 @@ pub const GlobalState = struct { }; // We first try to parse any action that we may be executing. - // We do not execute this in the lib because os.argv is not set. - if (comptime build_config.artifact != .lib) { - self.action = try cli_action.Action.detectCLI(self.alloc); + 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 = {} }; - } + // 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, diff --git a/src/main_c.zig b/src/main_c.zig index ec3441582..992c20d19 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -42,13 +42,33 @@ const Info = extern struct { /// one global state but it has zero practical benefit. export fn ghostty_init() 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; + main.state.init() catch |err| { std.log.err("failed to initialize ghostty error={}", .{err}); return 1; }; + 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}); + std.os.exit(1); + }; +} + +/// Return metadata about Ghostty, such as version, build mode, etc. export fn ghostty_info() Info { return .{ .mode = switch (builtin.mode) { From 718c8d7ac88d609b1798e01c9e6846eafb0326a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Sep 2023 12:38:26 -0700 Subject: [PATCH 4/6] main: disable stderr logging by default for lib --- src/main.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.zig b/src/main.zig index 37e35e490..bd0a25bd6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -233,6 +233,11 @@ pub const GlobalState = struct { // output. if (self.action != null) self.logging = .{ .disabled = {} }; + // For lib mode we always disable stderr logging by default. + if (comptime build_config.app_runtime == .none) { + 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 From bd528f5c116596bc2b822f589a1f88277211c518 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Sep 2023 12:43:35 -0700 Subject: [PATCH 5/6] macos: set the proper env var --- macos/Ghostty-Info.plist | 7 ++++--- macos/Sources/main.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index de2c98bf9..eb64fed93 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -2,9 +2,10 @@ - GHOSTTY_MAC_APP - 1 LSEnvironment - + + GHOSTTY_MAC_APP + 1 + diff --git a/macos/Sources/main.swift b/macos/Sources/main.swift index 9a462b440..990ef8ef1 100644 --- a/macos/Sources/main.swift +++ b/macos/Sources/main.swift @@ -6,7 +6,7 @@ import GhosttyKit // 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"] == "") { +if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") { ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv) exit(1) } From ea4bc95f43320acffdf9a3cd1638b14beb6d02bd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 20 Sep 2023 13:02:06 -0700 Subject: [PATCH 6/6] os: appendEnv --- src/os/env.zig | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/os/main.zig | 1 + 2 files changed, 49 insertions(+) create mode 100644 src/os/env.zig diff --git a/src/os/env.zig b/src/os/env.zig new file mode 100644 index 000000000..d6c2970c5 --- /dev/null +++ b/src/os/env.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; + +/// Append a value to an environment variable such as PATH. +/// The returned value is always allocated so it must be freed. +pub fn appendEnv( + alloc: Allocator, + current: []const u8, + value: []const u8, +) ![]u8 { + // If there is no prior value, we return it as-is + if (current.len == 0) return try alloc.dupe(u8, value); + + // Otherwise we must prefix. + const sep = switch (builtin.os.tag) { + .windows => ";", + else => ":", + }; + + return try std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ + current, + sep, + value, + }); +} + +test "appendEnv empty" { + const testing = std.testing; + const alloc = testing.allocator; + + const result = try appendEnv(alloc, "", "foo"); + defer alloc.free(result); + try testing.expectEqualStrings(result, "foo"); +} + +test "appendEnv existing" { + const testing = std.testing; + const alloc = testing.allocator; + + const result = try appendEnv(alloc, "a:b", "foo"); + defer alloc.free(result); + if (builtin.os.tag == .windows) { + try testing.expectEqualStrings(result, "a:b;foo"); + } else { + try testing.expectEqualStrings(result, "a:b:foo"); + } +} diff --git a/src/os/main.zig b/src/os/main.zig index b80c116e6..a9df1fca6 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -1,6 +1,7 @@ //! The "os" package contains utilities for interfacing with the operating //! system. +pub usingnamespace @import("env.zig"); pub usingnamespace @import("file.zig"); pub usingnamespace @import("flatpak.zig"); pub usingnamespace @import("homedir.zig");