diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index e793615d5..bf4c44ad0 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -724,7 +724,7 @@ pub const Surface = struct { /// Set the shape of the cursor. fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void { if ((comptime builtin.target.isDarwin()) and - !internal_os.macosVersionAtLeast(13, 0, 0)) + !internal_os.macos.isAtLeastVersion(13, 0, 0)) { // We only set our cursor if we're NOT on Mac, or if we are then the // macOS version is >= 13 (Ventura). On prior versions, glfw crashes diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index bb5309333..6069c825d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -14,6 +14,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const builtin = @import("builtin"); +const build_config = @import("../../build_config.zig"); const apprt = @import("../../apprt.zig"); const configpkg = @import("../../config.zig"); const input = @import("../../input.zig"); @@ -181,7 +182,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { } } - const default_id = "com.mitchellh.ghostty"; + const default_id = comptime build_config.bundle_id; break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id; }; diff --git a/src/apprt/gtk/ConfigErrorsWindow.zig b/src/apprt/gtk/ConfigErrorsWindow.zig index 6d4cda21b..3ff52908e 100644 --- a/src/apprt/gtk/ConfigErrorsWindow.zig +++ b/src/apprt/gtk/ConfigErrorsWindow.zig @@ -3,6 +3,7 @@ const ConfigErrors = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const build_config = @import("../../build_config.zig"); const configpkg = @import("../../config.zig"); const Config = configpkg.Config; @@ -53,7 +54,7 @@ fn init(self: *ConfigErrors, app: *App) !void { c.gtk_window_set_title(gtk_window, "Configuration Errors"); c.gtk_window_set_default_size(gtk_window, 600, 275); c.gtk_window_set_resizable(gtk_window, 0); - c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); + c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); // Set some state diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index aef67b308..9a361c228 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -5,6 +5,7 @@ const Surface = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const build_config = @import("../../build_config.zig"); const configpkg = @import("../../config.zig"); const apprt = @import("../../apprt.zig"); const font = @import("../../font/main.zig"); @@ -1149,7 +1150,7 @@ pub fn showDesktopNotification( defer c.g_object_unref(notification); c.g_notification_set_body(notification, body.ptr); - const icon = c.g_themed_icon_new("com.mitchellh.ghostty"); + const icon = c.g_themed_icon_new(build_config.bundle_id); defer c.g_object_unref(icon); c.g_notification_set_icon(notification, icon); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index e220ac03b..23265c101 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -103,7 +103,7 @@ pub fn init(self: *Window, app: *App) !void { // to disable this so that terminal programs can capture F10 (such as htop) c.gtk_window_set_handle_menubar_accel(gtk_window, 0); - c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); + c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id); // Apply class to color headerbar if window-theme is set to `ghostty` and // GTK version is before 4.16. The conditional is because above 4.16 diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig index f5bdf8a24..119e20a6c 100644 --- a/src/apprt/gtk/inspector.zig +++ b/src/apprt/gtk/inspector.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const build_config = @import("../../build_config.zig"); const App = @import("App.zig"); const Surface = @import("Surface.zig"); const TerminalWindow = @import("Window.zig"); @@ -141,7 +142,7 @@ const Window = struct { self.window = gtk_window; c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector"); c.gtk_window_set_default_size(gtk_window, 1000, 600); - c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); + c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id); // Initialize our imgui widget try self.imgui_widget.init(); diff --git a/src/build_config.zig b/src/build_config.zig index 715552e03..1448f9de5 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -103,6 +103,20 @@ pub const app_runtime: apprt.Runtime = config.app_runtime; pub const font_backend: font.Backend = config.font_backend; pub const renderer: rendererpkg.Impl = config.renderer; +/// The bundle ID for the app. This is used in many places and is currently +/// hardcoded here. We could make this configurable in the future if there +/// is a reason to do so. +/// +/// On macOS, this must match the App bundle ID. We can get that dynamically +/// via an API but I don't want to pay the cost of that at runtime. +/// +/// On GTK, this should match the various folders with resources. +/// +/// There are many places that don't use this variable so simply swapping +/// this variable is NOT ENOUGH to change the bundle ID. I just wanted to +/// avoid it in Zig coe as much as possible. +pub const bundle_id = "com.mitchellh.ghostty"; + /// True if we should have "slow" runtime safety checks. The initial motivation /// for this was terminal page/pagelist integrity checks. These were VERY /// slow but very thorough. But they made it so slow that the terminal couldn't diff --git a/src/config/Config.zig b/src/config/Config.zig index 0f8be9662..253e96420 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1813,9 +1813,10 @@ pub fn deinit(self: *Config) void { /// Load the configuration according to the default rules: /// /// 1. Defaults -/// 2. XDG Config File -/// 3. CLI flags -/// 4. Recursively defined configuration files +/// 2. XDG config dir +/// 3. "Application Support" directory (macOS only) +/// 4. CLI flags +/// 5. Recursively defined configuration files /// pub fn load(alloc_gpa: Allocator) !Config { var result = try default(alloc_gpa); @@ -2398,25 +2399,37 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { try self.expandPaths(std.fs.path.dirname(path).?); } -/// Load the configuration from the default configuration file. The default -/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. -pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { - const config_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); - defer alloc.free(config_path); - - self.loadFile(alloc, config_path) catch |err| switch (err) { +/// Load optional configuration file from `path`. All errors are ignored. +pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void { + self.loadFile(alloc, path) catch |err| switch (err) { error.FileNotFound => std.log.info( - "homedir config not found, not loading path={s}", - .{config_path}, + "optional config file not found, not loading path={s}", + .{path}, ), - else => std.log.warn( - "error reading config file, not loading err={} path={s}", - .{ err, config_path }, + "error reading optional config file, not loading err={} path={s}", + .{ err, path }, ), }; } +/// Load configurations from the default configuration files. The default +/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. +/// +/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` +/// is also loaded. +pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { + const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); + defer alloc.free(xdg_path); + self.loadOptionalFile(alloc, xdg_path); + + if (comptime builtin.os.tag == .macos) { + const app_support_path = try internal_os.macos.appSupportDir(alloc, "config"); + defer alloc.free(app_support_path); + self.loadOptionalFile(alloc, app_support_path); + } +} + /// Load and parse the CLI args. pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 071d4d530..b3df80538 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -141,7 +141,7 @@ fn logFn( // Initialize a logger. This is slow to do on every operation // but we shouldn't be logging too much. - const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope)); + const logger = macos.os.Log.create(build_config.bundle_id, @tagName(scope)); defer logger.release(); logger.log(std.heap.c_allocator, mac_level, format, args); } diff --git a/src/os/macos.zig b/src/os/macos.zig new file mode 100644 index 000000000..d405cd161 --- /dev/null +++ b/src/os/macos.zig @@ -0,0 +1,77 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const build_config = @import("../build_config.zig"); +const assert = std.debug.assert; +const objc = @import("objc"); +const Allocator = std.mem.Allocator; + +/// Verifies that the running macOS system version is at least the given version. +pub fn isAtLeastVersion(major: i64, minor: i64, patch: i64) bool { + comptime assert(builtin.target.isDarwin()); + + const NSProcessInfo = objc.getClass("NSProcessInfo").?; + const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); + return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{ + NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch }, + }); +} + +pub const AppSupportDirError = Allocator.Error || error{AppleAPIFailed}; + +/// Return the path to the application support directory for Ghostty +/// with the given sub path joined. This allocates the result using the +/// given allocator. +pub fn appSupportDir( + alloc: Allocator, + sub_path: []const u8, +) AppSupportDirError![]u8 { + comptime assert(builtin.target.isDarwin()); + + const NSFileManager = objc.getClass("NSFileManager").?; + const manager = NSFileManager.msgSend( + objc.Object, + objc.sel("defaultManager"), + .{}, + ); + + const url = manager.msgSend( + objc.Object, + objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"), + .{ + NSSearchPathDirectory.NSApplicationSupportDirectory, + NSSearchPathDomainMask.NSUserDomainMask, + @as(?*anyopaque, null), + true, + @as(?*anyopaque, null), + }, + ); + + // I don't think this is possible but just in case. + if (url.value == null) return error.AppleAPIFailed; + + // Get the UTF-8 string from the URL. + const path = url.getProperty(objc.Object, "path"); + const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse + return error.AppleAPIFailed; + const app_support_dir = std.mem.sliceTo(c_str, 0); + + return try std.fs.path.join(alloc, &.{ + app_support_dir, + build_config.bundle_id, + sub_path, + }); +} + +pub const NSOperatingSystemVersion = extern struct { + major: i64, + minor: i64, + patch: i64, +}; + +pub const NSSearchPathDirectory = enum(c_ulong) { + NSApplicationSupportDirectory = 14, +}; + +pub const NSSearchPathDomainMask = enum(c_ulong) { + NSUserDomainMask = 1, +}; diff --git a/src/os/macos_version.zig b/src/os/macos_version.zig deleted file mode 100644 index e0b21560e..000000000 --- a/src/os/macos_version.zig +++ /dev/null @@ -1,21 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const objc = @import("objc"); - -/// Verifies that the running macOS system version is at least the given version. -pub fn macosVersionAtLeast(major: i64, minor: i64, patch: i64) bool { - assert(builtin.target.isDarwin()); - - const NSProcessInfo = objc.getClass("NSProcessInfo").?; - const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); - return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{ - NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch }, - }); -} - -pub const NSOperatingSystemVersion = extern struct { - major: i64, - minor: i64, - patch: i64, -}; diff --git a/src/os/main.zig b/src/os/main.zig index 22765f546..073129300 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -8,7 +8,6 @@ const file = @import("file.zig"); const flatpak = @import("flatpak.zig"); const homedir = @import("homedir.zig"); const locale = @import("locale.zig"); -const macos_version = @import("macos_version.zig"); const mouse = @import("mouse.zig"); const openpkg = @import("open.zig"); const pipepkg = @import("pipe.zig"); @@ -21,6 +20,7 @@ pub const hostname = @import("hostname.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); +pub const macos = @import("macos.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); @@ -37,7 +37,6 @@ pub const freeTmpDir = file.freeTmpDir; pub const isFlatpak = flatpak.isFlatpak; pub const home = homedir.home; pub const ensureLocale = locale.ensureLocale; -pub const macosVersionAtLeast = macos_version.macosVersionAtLeast; pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open; pub const pipe = pipepkg.pipe;