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/Ghostty-Info.plist b/macos/Ghostty-Info.plist
index 253b170f1..eb64fed93 100644
--- a/macos/Ghostty-Info.plist
+++ b/macos/Ghostty-Info.plist
@@ -2,7 +2,10 @@
- NSMainNibFile
- MainMenu
+ LSEnvironment
+
+ GHOSTTY_MAC_APP
+ 1
+
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index bf290f623..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 */,
@@ -424,11 +428,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 +463,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",
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..990ef8ef1
--- /dev/null
+++ b/macos/Sources/main.swift
@@ -0,0 +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"] ?? "") == "") {
+ 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..bd0a25bd6 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,14 +226,16 @@ 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 = {} };
+
+ // 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
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) {
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");