diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index ebf728d83..e4a3e939c 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -187,6 +187,9 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp // Config could change keybindings, so update our menu syncMenuShortcuts() + // Config could change window appearance + syncAppearance() + // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance c.model.errors = state.configErrors() @@ -197,6 +200,23 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp } } + /// Sync the appearance of our app with the theme specified in the config. + private func syncAppearance() { + guard let theme = ghostty.windowTheme else { return } + switch (theme) { + case "dark": + let appearance = NSAppearance(named: .darkAqua) + NSApplication.shared.appearance = appearance + + case "light": + let appearance = NSAppearance(named: .aqua) + NSApplication.shared.appearance = appearance + + default: + NSApplication.shared.appearance = nil + } + } + //MARK: - Dock Menu private func reloadDockMenu() { diff --git a/macos/Sources/Features/Primary Window/PrimaryWindow.swift b/macos/Sources/Features/Primary Window/PrimaryWindow.swift index 51194ad91..0b199921b 100644 --- a/macos/Sources/Features/Primary Window/PrimaryWindow.swift +++ b/macos/Sources/Features/Primary Window/PrimaryWindow.swift @@ -15,6 +15,14 @@ class FocusedSurfaceWrapper { // such as non-native fullscreen. class PrimaryWindow: NSWindow { var focusedSurfaceWrapper: FocusedSurfaceWrapper = FocusedSurfaceWrapper() + + override var canBecomeKey: Bool { + return true + } + + override var canBecomeMain: Bool { + return true + } static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate, baseConfig: ghostty_surface_config_s? = nil) -> PrimaryWindow { let window = PrimaryWindow( @@ -53,12 +61,4 @@ class PrimaryWindow: NSWindow { return mask } - - override var canBecomeKey: Bool { - return true - } - - override var canBecomeMain: Bool { - return true - } } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index fe6ee42ae..55a5b7b50 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -71,7 +71,26 @@ extension Ghostty { _ = ghostty_config_get(config, &v, key, UInt(key.count)) return v; } - + + /// The window theme as a string. + var windowTheme: String? { + guard let config = self.config else { return nil } + var v: UnsafePointer? = nil + let key = "window-theme" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil } + guard let ptr = v else { return nil } + return String(cString: ptr) + } + + /// The background opacity. + var backgroundOpacity: Double { + guard let config = self.config else { return 1 } + var v: Double = 1 + let key = "background-opacity" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v; + } + init() { // Initialize ghostty global state. This happens once per process. guard ghostty_init() == GHOSTTY_SUCCESS else { diff --git a/src/config/Config.zig b/src/config/Config.zig index 629392436..79b9ea39d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -244,6 +244,14 @@ keybind: Keybinds = .{}, /// borders. @"window-decoration": bool = true, +/// The theme to use for the windows. The default is "system" which +/// means that whatever the system theme is will be used. This can +/// also be set to "light" or "dark" to force a specific theme regardless +/// of the system settings. +/// +/// This is currently only supported on macOS. +@"window-theme": WindowTheme = .system, + /// Whether to allow programs running in the terminal to read/write to /// the system clipboard (OSC 52, for googling). The default is to /// disallow clipboard reading but allow writing. @@ -1507,3 +1515,10 @@ pub const OSCColorReportFormat = enum { @"8-bit", @"16-bit", }; + +/// The default window theme. +pub const WindowTheme = enum { + system, + light, + dark, +}; diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 05fa5fe7d..504e98a87 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -19,7 +19,7 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { const value = fieldByKey(config, tag); switch (@TypeOf(value)) { ?[:0]const u8 => { - const ptr: *[*c]const u8 = @ptrCast(@alignCast(ptr_raw)); + const ptr: *?[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); ptr.* = if (value) |slice| @ptrCast(slice.ptr) else null; }, @@ -38,7 +38,14 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { ptr.* = @floatCast(value); }, - else => return false, + else => |T| switch (@typeInfo(T)) { + .Enum => { + const ptr: *[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @tagName(value); + }, + + else => return false, + }, } return true; @@ -74,3 +81,18 @@ test "u8" { try testing.expect(get(&c, .@"font-size", &cval)); try testing.expectEqual(@as(c_uint, 24), cval); } + +test "enum" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try Config.default(alloc); + defer c.deinit(); + c.@"window-theme" = .dark; + + var cval: [*:0]u8 = undefined; + try testing.expect(get(&c, .@"window-theme", @ptrCast(&cval))); + + const str = std.mem.sliceTo(cval, 0); + try testing.expectEqualStrings("dark", str); +}