From 4a8fd3d0fe72c20ff750d1e3fac27a398d3fb85a Mon Sep 17 00:00:00 2001 From: "Matthew Z." <87685252+che-3@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:19:49 -0400 Subject: [PATCH] Fix dock icon not auto-switching with system dark mode (#1928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix dock icon not auto-switching with system dark mode The automatic icon mode relied on the asset catalog to handle appearance-based icon selection, but the compiled Assets.car does not include dark variant renditions for AppIcon. This meant setting applicationIconImage to nil had no effect — the icon stayed on the light variant regardless of system appearance. Replace the nil-reset approach with an active KVO observer on NSApp.effectiveAppearance that programmatically swaps between AppIconDark and AppIconLight images when in automatic mode. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review feedback: fix async race and harden singleton - Add guard in async callback to skip stale updates after stopObserving() - Add private init() to prevent external instantiation of singleton - Remove unused .new KVO option Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: CHE-3 Co-authored-by: Claude Opus 4.6 (1M context) --- Sources/cmuxApp.swift | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 1cc1f9fc..711d6c9f 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -3695,14 +3695,14 @@ enum AppIconSettings { static func applyIcon(_ mode: AppIconMode) { switch mode { case .automatic: - // Let the asset catalog handle appearance-based icon selection (macOS 15+). - // Reset to the default bundle icon. - NSApplication.shared.applicationIconImage = nil + AppIconAppearanceObserver.shared.startObserving() case .light: + AppIconAppearanceObserver.shared.stopObserving() if let icon = NSImage(named: "AppIconLight") { NSApplication.shared.applicationIconImage = icon } case .dark: + AppIconAppearanceObserver.shared.stopObserving() if let icon = NSImage(named: "AppIconDark") { NSApplication.shared.applicationIconImage = icon } @@ -3710,6 +3710,37 @@ enum AppIconSettings { } } +final class AppIconAppearanceObserver: NSObject { + static let shared = AppIconAppearanceObserver() + private var observation: NSKeyValueObservation? + + private override init() { super.init() } + + func startObserving() { + applyIconForCurrentAppearance() + guard observation == nil else { return } + observation = NSApp.observe(\.effectiveAppearance, options: []) { [weak self] _, _ in + DispatchQueue.main.async { + guard let self, self.observation != nil else { return } + self.applyIconForCurrentAppearance() + } + } + } + + func stopObserving() { + observation?.invalidate() + observation = nil + } + + private func applyIconForCurrentAppearance() { + let isDark = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua + let imageName = isDark ? "AppIconDark" : "AppIconLight" + if let icon = NSImage(named: imageName) { + NSApplication.shared.applicationIconImage = icon + } + } +} + enum QuitWarningSettings { static let warnBeforeQuitKey = "warnBeforeQuitShortcut" static let defaultWarnBeforeQuit = true