diff --git a/include/ghostty.h b/include/ghostty.h index 813f81df2..681a4e9dc 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -537,6 +537,11 @@ typedef struct { ghostty_config_t config; } ghostty_action_config_change_s; +// apprt.action.ReloadConfig +typedef struct { + bool soft; +} ghostty_action_reload_config_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_NEW_WINDOW, @@ -572,7 +577,7 @@ typedef enum { GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, GHOSTTY_ACTION_COLOR_CHANGE, - GHOSTTY_ACTION_CONFIG_CHANGE_CONDITIONAL_STATE, + GHOSTTY_ACTION_RELOAD_CONFIG, GHOSTTY_ACTION_CONFIG_CHANGE, } ghostty_action_tag_e; @@ -598,6 +603,7 @@ typedef union { ghostty_action_secure_input_e secure_input; ghostty_action_key_sequence_s key_sequence; ghostty_action_color_change_s color_change; + ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; } ghostty_action_u; @@ -607,7 +613,6 @@ typedef struct { } ghostty_action_s; typedef void (*ghostty_runtime_wakeup_cb)(void*); -typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*); typedef void (*ghostty_runtime_read_clipboard_cb)(void*, ghostty_clipboard_e, void*); @@ -630,7 +635,6 @@ typedef struct { bool supports_selection_clipboard; ghostty_runtime_wakeup_cb wakeup_cb; ghostty_runtime_action_cb action_cb; - ghostty_runtime_reload_config_cb reload_config_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb; ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb; @@ -668,7 +672,7 @@ void ghostty_app_set_focus(ghostty_app_t, bool); bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); void ghostty_app_keyboard_changed(ghostty_app_t); void ghostty_app_open_config(ghostty_app_t); -void ghostty_app_reload_config(ghostty_app_t); +void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); bool ghostty_app_needs_confirm_quit(ghostty_app_t); bool ghostty_app_has_global_keybinds(ghostty_app_t); @@ -679,6 +683,7 @@ void ghostty_surface_free(ghostty_surface_t); void* ghostty_surface_userdata(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); +void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_draw(ghostty_surface_t); diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index bc7f468e0..7db98acf3 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CE62C33060F00C57AD3 /* man in Resources */ = {isa = PBXBuildFile; fileRef = A5985CE52C33060F00C57AD3 /* man */; }; + A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */; }; A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; @@ -141,6 +142,7 @@ A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitNode.swift; sourceTree = ""; }; A5985CD62C320C4500C57AD3 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; A5985CE52C33060F00C57AD3 /* man */ = {isa = PBXFileReference; lastKnownFileType = folder; name = man; path = "../zig-out/share/man"; sourceTree = ""; }; + A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAppearance+Extension.swift"; sourceTree = ""; }; A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; @@ -249,6 +251,7 @@ A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, + A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, @@ -611,6 +614,7 @@ A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, + A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index b9bebe542..52a535365 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -65,7 +65,6 @@ extension Ghostty { supports_selection_clipboard: false, wakeup_cb: { userdata in App.wakeup(userdata) }, action_cb: { app, target, action in App.action(app!, target: target, action: action) }, - reload_config_cb: { userdata in App.reloadConfig(userdata) }, read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, @@ -142,9 +141,47 @@ extension Ghostty { ghostty_app_open_config(app) } - func reloadConfig() { + /// Reload the configuration. + func reloadConfig(soft: Bool = false) { guard let app = self.app else { return } - ghostty_app_reload_config(app) + + // Soft updates just call with our existing config + if (soft) { + ghostty_app_update_config(app, config.config!) + return + } + + // Hard or full updates have to reload the full configuration + let newConfig = Config() + guard newConfig.loaded else { + Ghostty.logger.warning("failed to reload configuration") + return + } + + ghostty_app_update_config(app, newConfig.config!) + + // We can only set our config after updating it so that we don't free + // memory that may still be in use + self.config = newConfig + } + + func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) { + // Soft updates just call with our existing config + if (soft) { + ghostty_surface_update_config(surface, config.config!) + return + } + + // Hard or full updates have to reload the full configuration. + // NOTE: We never set this on self.config because this is a surface-only + // config. We free it after the call. + let newConfig = Config() + guard newConfig.loaded else { + Ghostty.logger.warning("failed to reload configuration") + return + } + + ghostty_surface_update_config(surface, newConfig.config!) } /// Request that the given surface is closed. This will trigger the full normal surface close event @@ -237,7 +274,6 @@ extension Ghostty { static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {} - static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil } static func readClipboard( _ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, @@ -365,21 +401,6 @@ extension Ghostty { ) } - static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { - let newConfig = Config() - guard newConfig.loaded else { - AppDelegate.logger.warning("failed to reload configuration") - return nil - } - - // Assign the new config. This will automatically free the old config. - // It is safe to free the old config from within this function call. - let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - state.config = newConfig - - return newConfig.config - } - static func wakeup(_ userdata: UnsafeMutableRawPointer?) { let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() @@ -514,6 +535,9 @@ extension Ghostty { case GHOSTTY_ACTION_CONFIG_CHANGE: configChange(app, target: target, v: action.action.config_change) + case GHOSTTY_ACTION_RELOAD_CONFIG: + configReload(app, target: target, v: action.action.reload_config) + case GHOSTTY_ACTION_COLOR_CHANGE: colorChange(app, target: target, change: action.action.color_change) @@ -1150,6 +1174,30 @@ extension Ghostty { } } + private static func configReload( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_reload_config_s) + { + logger.info("config reload notification") + + guard let app_ud = ghostty_app_userdata(app) else { return } + let ghostty = Unmanaged.fromOpaque(app_ud).takeUnretainedValue() + + switch (target.tag) { + case GHOSTTY_TARGET_APP: + ghostty.reloadConfig(soft: v.soft) + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + ghostty.reloadConfig(surface: surface, soft: v.soft) + + default: + assertionFailure() + } + } + private static func configChange( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Helpers/NSAppearance+Extension.swift b/macos/Sources/Helpers/NSAppearance+Extension.swift new file mode 100644 index 000000000..336125574 --- /dev/null +++ b/macos/Sources/Helpers/NSAppearance+Extension.swift @@ -0,0 +1,8 @@ +import Cocoa + +extension NSAppearance { + /// Returns true if the appearance is some kind of dark. + var isDark: Bool { + return name.rawValue.lowercased().contains("dark") + } +} diff --git a/src/App.zig b/src/App.zig index ebf257f04..57b30ada0 100644 --- a/src/App.zig +++ b/src/App.zig @@ -12,7 +12,8 @@ const apprt = @import("apprt.zig"); const Surface = @import("Surface.zig"); const tracy = @import("tracy"); const input = @import("input.zig"); -const Config = @import("config.zig").Config; +const configpkg = @import("config.zig"); +const Config = configpkg.Config; const BlockingQueue = @import("datastruct/main.zig").BlockingQueue; const renderer = @import("renderer.zig"); const font = @import("font/main.zig"); @@ -239,7 +240,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { while (self.mailbox.pop()) |message| { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { - .reload_config => try self.reloadConfig(rt_app), .open_config => try self.performAction(rt_app, .open_config), .new_window => |msg| try self.newWindow(rt_app, msg), .close => |surface| self.closeSurface(surface), @@ -260,14 +260,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { } } -pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void { - log.debug("reloading configuration", .{}); - if (try rt_app.reloadConfig()) |new| { - log.debug("new configuration received, applying", .{}); - try self.updateConfig(rt_app, new); - } -} - pub fn closeSurface(self: *App, surface: *Surface) void { if (!self.hasSurface(surface)) return; surface.close(); @@ -402,7 +394,7 @@ pub fn performAction( .quit => self.setQuit(), .new_window => try self.newWindow(rt_app, .{ .parent = null }), .open_config => try rt_app.performAction(.app, .open_config, {}), - .reload_config => try self.reloadConfig(rt_app), + .reload_config => try rt_app.performAction(.app, .reload_config, .{}), .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), .toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}), .toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}), @@ -462,10 +454,6 @@ fn hasSurface(self: *const App, surface: *const Surface) bool { /// The message types that can be sent to the app thread. pub const Message = union(enum) { - /// Reload the configuration for the entire app and propagate it to - /// all the active surfaces. - reload_config: void, - // Open the configuration file open_config: void, diff --git a/src/Surface.zig b/src/Surface.zig index ebeac4b97..ee2fe1ac5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1065,8 +1065,8 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void { fn notifyConfigConditionalState(self: *Surface) void { self.rt_app.performAction( .{ .surface = self }, - .config_change_conditional_state, - {}, + .reload_config, + .{ .soft = true }, ) catch |err| { log.warn("failed to notify app of config state change err={}", .{err}); }; @@ -1077,8 +1077,22 @@ fn notifyConfigConditionalState(self: *Surface) void { /// or other surfaces. pub fn updateConfig( self: *Surface, - config: *const configpkg.Config, + original: *const configpkg.Config, ) !void { + // Apply our conditional state. If we fail to apply the conditional state + // then we log and attempt to move forward with the old config. + var config_: ?configpkg.Config = original.changeConditionalState( + self.config_conditional_state, + ) catch |err| err: { + log.warn("failed to apply conditional state to config err={}", .{err}); + break :err null; + }; + defer if (config_) |*c| c.deinit(); + + // We want a config pointer for everything so we get that either + // based on our conditional state or the original config. + const config: *const configpkg.Config = if (config_) |*c| c else original; + // Update our new derived config immediately const derived = DerivedConfig.init(self.alloc, config) catch |err| { // If the derivation fails then we just log and return. We don't diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 136f5fd7e..527535ffa 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -194,12 +194,14 @@ pub const Action = union(Key) { /// such as OSC 10/11. color_change: ColorChange, - /// The state of conditionals in the configuration has changed, so - /// the configuration should be reloaded. The apprt doesn't need - /// to do a full physical reload; it should call the - /// `changeConditionalState` function and then `updateConfig` - /// on the app or surface. - config_change_conditional_state, + /// A request to reload the configuration. The reload request can be + /// from a user or for some internal reason. The reload request may + /// request it is a soft reload or a full reload. See the struct for + /// more documentation. + /// + /// The configuration should be passed to updateConfig either at the + /// app or surface level depending on the target. + reload_config: ReloadConfig, /// The configuration has changed. The value is a pointer to the new /// configuration. The pointer is only valid for the duration of the @@ -250,7 +252,7 @@ pub const Action = union(Key) { secure_input, key_sequence, color_change, - config_change_conditional_state, + reload_config, config_change, }; @@ -514,6 +516,13 @@ pub const ColorKind = enum(c_int) { _, }; +pub const ReloadConfig = extern struct { + /// A soft reload means that the configuration doesn't need to be + /// read off disk, but libghostty needs the full config again so call + /// updateConfig with it. + soft: bool = false, +}; + pub const ConfigChange = struct { config: *const configpkg.Config, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 5288c4a7b..92137e6d7 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -47,11 +47,6 @@ pub const App = struct { /// Callback called to handle an action. action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void, - /// Reload the configuration and return the new configuration. - /// The old configuration can be freed immediately when this is - /// called. - reload_config: *const fn (AppUD) callconv(.C) ?*const Config, - /// Read the clipboard value. The return value must be preserved /// by the host until the next call. If there is no valid clipboard /// value then this should return null. @@ -375,16 +370,6 @@ pub const App = struct { } } - pub fn reloadConfig(self: *App) !?*const Config { - // Reload - if (self.opts.reload_config(self.opts.userdata)) |new| { - self.config = new; - return self.config; - } - - return null; - } - pub fn wakeup(self: App) void { self.opts.wakeup(self.opts.userdata); } @@ -465,30 +450,6 @@ pub const App = struct { }, }, - .config_change_conditional_state => switch (target) { - .app => {}, - .surface => |surface| action: { - // Build our new configuration. We can free the memory - // immediately after because the surface will derive any - // values it needs to. - var new_config = self.config.changeConditionalState( - surface.config_conditional_state, - ) catch |err| { - // Not a big deal if we error... we just don't update - // the config. We log the error and move on. - log.warn("error changing config conditional state err={}", .{err}); - break :action; - }; - defer new_config.deinit(); - - // Update our surface. - surface.updateConfig(&new_config) catch |err| { - log.warn("error updating surface config for state change err={}", .{err}); - break :action; - }; - }, - }, - else => {}, } } @@ -1374,10 +1335,14 @@ pub const CAPI = struct { }; } - /// Reload the configuration. - export fn ghostty_app_reload_config(v: *App) void { - _ = v.core_app.reloadConfig(v) catch |err| { - log.err("error reloading config err={}", .{err}); + /// Update the configuration to the provided config. This will propagate + /// to all surfaces as well. + export fn ghostty_app_update_config( + v: *App, + config: *const Config, + ) void { + v.core_app.updateConfig(v, config) catch |err| { + log.err("error updating config err={}", .{err}); return; }; } @@ -1434,6 +1399,17 @@ pub const CAPI = struct { return surface.newSurfaceOptions(); } + /// Update the configuration to the provided config for only this surface. + export fn ghostty_surface_update_config( + surface: *Surface, + config: *const Config, + ) void { + surface.core_surface.updateConfig(config) catch |err| { + log.err("error updating config err={}", .{err}); + return; + }; + } + /// Returns true if the surface needs to confirm quitting. export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool { return surface.core_surface.needsConfirmQuit(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 19be46778..e793615d5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -200,6 +200,8 @@ pub const App = struct { }), }, + .reload_config => try self.reloadConfig(target, value), + // Unimplemented .new_split, .goto_split, @@ -225,7 +227,6 @@ pub const App = struct { .renderer_health, .color_change, .pwd, - .config_change_conditional_state, .config_change, => log.info("unimplemented action={}", .{action}), } @@ -236,16 +237,34 @@ pub const App = struct { /// successful return. /// /// The returned pointer value is only valid for a stable self pointer. - pub fn reloadConfig(self: *App) !?*const Config { + fn reloadConfig( + self: *App, + target: apprt.action.Target, + opts: apprt.action.ReloadConfig, + ) !void { + if (opts.soft) { + switch (target) { + .app => try self.app.updateConfig(self, &self.config), + .surface => |core_surface| try core_surface.updateConfig( + &self.config, + ), + } + return; + } + // Load our configuration var config = try Config.load(self.app.alloc); errdefer config.deinit(); + // Call into our app to update + switch (target) { + .app => try self.app.updateConfig(self, &config), + .surface => |core_surface| try core_surface.updateConfig(&config), + } + // Update the existing config, be sure to clean up the old one. self.config.deinit(); self.config = config; - - return &self.config; } /// Toggle the window to fullscreen mode. diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index fa8a73830..0cee1938e 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -462,7 +462,8 @@ pub fn performAction( .equalize_splits => self.equalizeSplits(target), .goto_split => self.gotoSplit(target, value), .open_config => try configpkg.edit.open(self.core_app.alloc), - .config_change_conditional_state => self.configChangeConditionalState(target), + .config_change => self.configChange(value.config), + .reload_config => try self.reloadConfig(target, value), .inspector => self.controlInspector(target, value), .desktop_notification => self.showDesktopNotification(target, value), .set_title => try self.setTitle(target, value), @@ -488,7 +489,6 @@ pub fn performAction( .render_inspector, .renderer_health, .color_change, - .config_change, => log.warn("unimplemented action={}", .{action}), } } @@ -818,48 +818,9 @@ fn showDesktopNotification( c.g_application_send_notification(g_app, n.body.ptr, notification); } -fn configChangeConditionalState( - self: *App, - target: apprt.Target, -) void { - const surface: *CoreSurface = switch (target) { - .app => return, - .surface => |v| v, - }; +fn configChange(self: *App, new_config: *const Config) void { + _ = new_config; - // Build our new configuration. We can free the memory - // immediately after because the surface will derive any - // values it needs to. - var new_config = self.config.changeConditionalState( - surface.config_conditional_state, - ) catch |err| { - // Not a big deal if we error... we just don't update - // the config. We log the error and move on. - log.warn("error changing config conditional state err={}", .{err}); - return; - }; - defer new_config.deinit(); - - // Update our surface. - surface.updateConfig(&new_config) catch |err| { - log.warn("error updating surface config for state change err={}", .{err}); - return; - }; -} - -/// Reload the configuration. This should return the new configuration. -/// The old value can be freed immediately at this point assuming a -/// successful return. -/// -/// The returned pointer value is only valid for a stable self pointer. -pub fn reloadConfig(self: *App) !?*const Config { - // Load our configuration - var config = try Config.load(self.core_app.alloc); - errdefer config.deinit(); - - // Update the existing config, be sure to clean up the old one. - self.config.deinit(); - self.config = config; self.syncConfigChanges() catch |err| { log.warn("error handling configuration changes err={}", .{err}); }; @@ -870,8 +831,36 @@ pub fn reloadConfig(self: *App) !?*const Config { if (surface.container.window()) |window| window.onConfigReloaded(); } } +} - return &self.config; +pub fn reloadConfig( + self: *App, + target: apprt.action.Target, + opts: apprt.action.ReloadConfig, +) !void { + if (opts.soft) { + switch (target) { + .app => try self.core_app.updateConfig(self, &self.config), + .surface => |core_surface| try core_surface.updateConfig( + &self.config, + ), + } + return; + } + + // Load our configuration + var config = try Config.load(self.core_app.alloc); + errdefer config.deinit(); + + // Call into our app to update + switch (target) { + .app => try self.core_app.updateConfig(self, &config), + .surface => |core_surface| try core_surface.updateConfig(&config), + } + + // Update the existing config, be sure to clean up the old one. + self.config.deinit(); + self.config = config; } /// Call this anytime the configuration changes. @@ -1439,9 +1428,9 @@ fn gtkActionReloadConfig( ud: ?*anyopaque, ) callconv(.C) void { const self: *App = @ptrCast(@alignCast(ud orelse return)); - _ = self.core_app.mailbox.push(.{ - .reload_config = {}, - }, .{ .forever = {} }); + self.reloadConfig(.app, .{}) catch |err| { + log.err("error reloading configuration: {}", .{err}); + }; } fn gtkActionQuit( diff --git a/src/apprt/gtk/ConfigErrorsWindow.zig b/src/apprt/gtk/ConfigErrorsWindow.zig index 3f8ba3205..6d4cda21b 100644 --- a/src/apprt/gtk/ConfigErrorsWindow.zig +++ b/src/apprt/gtk/ConfigErrorsWindow.zig @@ -203,7 +203,7 @@ const ButtonsView = struct { fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { const self: *ConfigErrors = @ptrCast(@alignCast(ud)); - _ = self.app.reloadConfig() catch |err| { + self.app.reloadConfig(.app, .{}) catch |err| { log.warn("error reloading config error={}", .{err}); return; }; diff --git a/src/config/Config.zig b/src/config/Config.zig index 3d8faa135..4aa0beb79 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1554,10 +1554,10 @@ keybind: Keybinds = .{}, /// The proxy icon is only visible with the native macOS titlebar style. /// /// Valid values are: -/// +/// /// * `visible` - Show the proxy icon. /// * `hidden` - Hide the proxy icon. -/// +/// /// The default value is `visible`. /// /// This setting can be changed at runtime and will affect all currently @@ -2752,17 +2752,19 @@ pub fn finalize(self: *Config) !void { // We always load the theme first because it may set other fields // in our config. if (self.theme) |theme| { - // If we have different light vs dark mode themes, disable - // window-theme = auto since that breaks it. - if (!std.mem.eql(u8, theme.light, theme.dark)) { - // This setting doesn't make sense with different light/dark themes - // because it'll force the theme based on the Ghostty theme. - if (self.@"window-theme" == .auto) self.@"window-theme" = .system; - } + const different = !std.mem.eql(u8, theme.light, theme.dark); // Warning: loadTheme will deinit our existing config and replace // it so all memory from self prior to this point will be freed. try self.loadTheme(theme); + + // If we have different light vs dark mode themes, disable + // window-theme = auto since that breaks it. + if (different) { + // This setting doesn't make sense with different light/dark themes + // because it'll force the theme based on the Ghostty theme. + if (self.@"window-theme" == .auto) self.@"window-theme" = .system; + } } const alloc = self._arena.?.allocator(); @@ -5380,3 +5382,21 @@ test "theme loading correct light/dark" { }, new.background); } } + +test "theme specifying light/dark changes window-theme from auto" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--theme=light:foo,dark:bar", + "--window-theme=auto", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + try testing.expect(cfg.@"window-theme" == .system); + } +}