diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 36716007afa5..01ad608b1bab 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -142,6 +142,7 @@ A5001800A1B2C3D4E5F60718 /* WindowBackdropController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001801A1B2C3D4E5F60718 /* WindowBackdropController.swift */; }; F57072635F25EBCA741E125D /* SidebarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1614EAD3CCF70A177A51BD1 /* SidebarState.swift */; }; A8CBA43C2DA5AB9E3A1E65A4 /* CommandPaletteSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A09EB2E92203B2E95923A7 /* CommandPaletteSearch.swift */; }; + B84BD7AD94EE485B8DDAB6FF /* GhosttySurfaceConfigurationRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */; }; 0D56BE882EAD4B67AC44F96D /* TerminalDirectoryOpenSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */; }; A72C9F4179B54DF38E99A021 /* CmuxCLIPathInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */; }; E8BA79E246754A8B99A0B823 /* ScreenIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */; }; @@ -612,6 +613,7 @@ A5001801A1B2C3D4E5F60718 /* WindowBackdropController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windowing/WindowBackdropController.swift; sourceTree = ""; }; D1614EAD3CCF70A177A51BD1 /* SidebarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarState.swift; sourceTree = ""; }; 38A09EB2E92203B2E95923A7 /* CommandPaletteSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette/CommandPaletteSearch.swift; sourceTree = ""; }; + 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GhosttySurfaceConfigurationRefresh.swift; sourceTree = ""; }; 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/TerminalDirectoryOpenSupport.swift; sourceTree = ""; }; 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CmuxCLIPathInstaller.swift; sourceTree = ""; }; 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/ScreenIdentity.swift; sourceTree = ""; }; @@ -1118,6 +1120,7 @@ A5001801A1B2C3D4E5F60718 /* WindowBackdropController.swift */, D1614EAD3CCF70A177A51BD1 /* SidebarState.swift */, 38A09EB2E92203B2E95923A7 /* CommandPaletteSearch.swift */, + 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */, 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */, 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */, 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */, @@ -1748,6 +1751,7 @@ A5001800A1B2C3D4E5F60718 /* WindowBackdropController.swift in Sources */, F57072635F25EBCA741E125D /* SidebarState.swift in Sources */, A8CBA43C2DA5AB9E3A1E65A4 /* CommandPaletteSearch.swift in Sources */, + B84BD7AD94EE485B8DDAB6FF /* GhosttySurfaceConfigurationRefresh.swift in Sources */, 0D56BE882EAD4B67AC44F96D /* TerminalDirectoryOpenSupport.swift in Sources */, A72C9F4179B54DF38E99A021 /* CmuxCLIPathInstaller.swift in Sources */, E8BA79E246754A8B99A0B823 /* ScreenIdentity.swift in Sources */, diff --git a/Sources/App/GhosttySurfaceConfigurationRefresh.swift b/Sources/App/GhosttySurfaceConfigurationRefresh.swift new file mode 100644 index 000000000000..3887daf302ba --- /dev/null +++ b/Sources/App/GhosttySurfaceConfigurationRefresh.swift @@ -0,0 +1,18 @@ +@MainActor +enum GhosttySurfaceConfigurationRefresh { + nonisolated static let forceRefreshReason = "appDelegate.refreshAfterGhosttyConfigReload" + + static func applyAfterAppConfigReload( + to surface: ghostty_surface_t?, + source: String, + reloadSurfaceConfiguration: (ghostty_surface_t, Bool, String) -> Void, + refreshHostBackground: () -> Void, + forceRefresh: (String) -> Void + ) { + if let surface { + reloadSurfaceConfiguration(surface, true, source) + } + refreshHostBackground() + forceRefresh(forceRefreshReason) + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 2db737ddd498..19099d6d78bc 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4790,8 +4790,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent func refreshTerminalSurfacesAfterGhosttyConfigReload(source: String) { var refreshedCount = 0 forEachTerminalPanel { terminalPanel in - terminalPanel.hostedView.refreshHostBackgroundAfterGhosttyConfigReload() - terminalPanel.surface.forceRefresh(reason: "appDelegate.refreshAfterGhosttyConfigReload") + let liveSurface = terminalPanel.surface.liveSurfaceForGhosttyAccess( + reason: "appDelegate.refreshAfterGhosttyConfigReload" + ) + GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload( + to: liveSurface, + source: source, + reloadSurfaceConfiguration: { surface, soft, source in + GhosttyApp.shared.reloadSurfaceConfiguration(surface, soft: soft, source: source) + }, + refreshHostBackground: { + terminalPanel.hostedView.refreshHostBackgroundAfterGhosttyConfigReload() + }, + forceRefresh: { reason in + terminalPanel.surface.forceRefresh(reason: reason) + } + ) refreshedCount += 1 } #if DEBUG diff --git a/cmuxTests/AppearanceSettingsTests.swift b/cmuxTests/AppearanceSettingsTests.swift index e999dd04f403..41c168c8d4c8 100644 --- a/cmuxTests/AppearanceSettingsTests.swift +++ b/cmuxTests/AppearanceSettingsTests.swift @@ -10,6 +10,56 @@ import SwiftUI @MainActor final class AppearanceSettingsTests: XCTestCase { + func testAppConfigReloadRefreshUpdatesSurfaceConfigBeforeRedraw() throws { + let fakeSurface = try XCTUnwrap(UnsafeMutableRawPointer(bitPattern: 0x3851)) + var events: [String] = [] + + GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload( + to: fakeSurface, + source: "appearanceSync:test", + reloadSurfaceConfiguration: { surface, soft, source in + XCTAssertEqual(surface, fakeSurface) + XCTAssertTrue(soft) + events.append("reload:\(source)") + }, + refreshHostBackground: { + events.append("host-background") + }, + forceRefresh: { reason in + events.append("force-refresh:\(reason)") + } + ) + + XCTAssertEqual(events, [ + "reload:appearanceSync:test", + "host-background", + "force-refresh:\(GhosttySurfaceConfigurationRefresh.forceRefreshReason)" + ]) + } + + func testAppConfigReloadRefreshSkipsSurfaceConfigUpdateWhenSurfaceIsUnavailable() { + var events: [String] = [] + + GhosttySurfaceConfigurationRefresh.applyAfterAppConfigReload( + to: nil, + source: "appearanceSync:teardown", + reloadSurfaceConfiguration: { _, _, _ in + events.append("reload") + }, + refreshHostBackground: { + events.append("host-background") + }, + forceRefresh: { reason in + events.append("force-refresh:\(reason)") + } + ) + + XCTAssertEqual(events, [ + "host-background", + "force-refresh:\(GhosttySurfaceConfigurationRefresh.forceRefreshReason)" + ]) + } + func testResolvedModeDefaultsToSystemWhenUnset() { let suiteName = "AppearanceSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else {