Merge pull request #500 from mitchellh/macos-cli

macos: Ghostty binary is also a CLI app
This commit is contained in:
Mitchell Hashimoto
2023-09-20 13:40:26 -07:00
committed by GitHub
9 changed files with 132 additions and 11 deletions

View File

@ -351,6 +351,7 @@ typedef struct {
// Published API // Published API
int ghostty_init(void); int ghostty_init(void);
void ghostty_cli_main(uintptr_t, char **);
ghostty_info_s ghostty_info(void); ghostty_info_s ghostty_info(void);
ghostty_config_t ghostty_config_new(); ghostty_config_t ghostty_config_new();

View File

@ -2,7 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSMainNibFile</key> <key>LSEnvironment</key>
<string>MainMenu</string> <dict>
<key>GHOSTTY_MAC_APP</key>
<string>1</string>
</dict>
</dict> </dict>
</plist> </plist>

View File

@ -32,6 +32,7 @@
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; }; A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.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 */; }; 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 */; }; A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* PrimaryView.swift */; };
A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; }; A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -65,6 +66,7 @@
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; }; A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; }; A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = "<group>"; }; A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = "<group>"; };
A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; }; A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -128,6 +130,7 @@
A54CD6ED299BEB14008C95BB /* Sources */ = { A54CD6ED299BEB14008C95BB /* Sources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A5FEB2FF2ABB69450068369E /* main.swift */,
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */, A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */,
857F63802A5E64F200CA4815 /* MainMenu.xib */, 857F63802A5E64F200CA4815 /* MainMenu.xib */,
A53426362A7DC53000EBB7A2 /* Features */, A53426362A7DC53000EBB7A2 /* Features */,
@ -279,6 +282,7 @@
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */, A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */,
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */, A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */,
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */,
@ -424,11 +428,13 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
EXECUTABLE_NAME = ghostty;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_FILE = "Ghostty-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@ -457,12 +463,14 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
EXECUTABLE_NAME = ghostty;
GCC_OPTIMIZATION_LEVEL = fast; GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_FILE = "Ghostty-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",

View File

@ -2,7 +2,6 @@ import AppKit
import OSLog import OSLog
import GhosttyKit import GhosttyKit
@NSApplicationMain
class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyAppStateDelegate { class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyAppStateDelegate {
// The application logger. We should probably move this at some point to a dedicated // The application logger. We should probably move this at some point to a dedicated
// class/struct but for now it lives here! 🤷 // class/struct but for now it lives here! 🤷

14
macos/Sources/main.swift Normal file
View File

@ -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)

View File

@ -23,7 +23,13 @@ const Ghostty = @import("main_c.zig").Ghostty;
/// rely on allocators being passed in as parameters. /// rely on allocators being passed in as parameters.
pub var state: GlobalState = undefined; 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 // We first start by initializing our global state. This will setup
// process-level state we need to run the terminal. The reason we use // 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; // a global is because the C API needs to be able to access this state;
@ -67,6 +73,24 @@ pub fn main() !void {
return; return;
} }
if (comptime build_config.app_runtime == .none) {
const stdout = std.io.getStdOut().writer();
try stdout.print("Usage: ghostty +<action> [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 // Create our app state
var app = try App.create(alloc); var app = try App.create(alloc);
defer app.destroy(); defer app.destroy();
@ -156,6 +180,7 @@ pub const GlobalState = struct {
stderr: void, stderr: void,
}; };
/// Initialize the global state.
pub fn init(self: *GlobalState) !void { pub fn init(self: *GlobalState) !void {
// Initialize ourself to nothing so we don't have any extra state. // Initialize ourself to nothing so we don't have any extra state.
// IMPORTANT: this MUST be initialized before any log output because // 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 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. self.action = try cli_action.Action.detectCLI(self.alloc);
if (comptime build_config.artifact != .lib) {
self.action = try cli_action.Action.detectCLI(self.alloc);
// If we have an action executing, we disable logging by default // If we have an action executing, we disable logging by default
// since we write to stderr we don't want logs messing up our // since we write to stderr we don't want logs messing up our
// output. // output.
if (self.action != null) self.logging = .{ .disabled = {} }; 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 // I don't love the env var name but I don't have it in my heart

View File

@ -42,13 +42,33 @@ const Info = extern struct {
/// one global state but it has zero practical benefit. /// one global state but it has zero practical benefit.
export fn ghostty_init() 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
// 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| { main.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;
}; };
return 0; 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 { export fn ghostty_info() Info {
return .{ return .{
.mode = switch (builtin.mode) { .mode = switch (builtin.mode) {

48
src/os/env.zig Normal file
View File

@ -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");
}
}

View File

@ -1,6 +1,7 @@
//! The "os" package contains utilities for interfacing with the operating //! The "os" package contains utilities for interfacing with the operating
//! system. //! system.
pub usingnamespace @import("env.zig");
pub usingnamespace @import("file.zig"); pub usingnamespace @import("file.zig");
pub usingnamespace @import("flatpak.zig"); pub usingnamespace @import("flatpak.zig");
pub usingnamespace @import("homedir.zig"); pub usingnamespace @import("homedir.zig");