From 3f964ae0ef0bde0eb334e6dd0cc685f069b7bcb5 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Fri, 6 Feb 2026 10:38:47 +0200 Subject: [PATCH 1/3] Fix macOS context menu always appearing in dark mode - Add systemAppearance() helper to read system-wide theme from UserDefaults - Set NSMenu.appearance to match system theme instead of menu bar appearance - Update menu appearance when theme changes in MenuBarAppearanceObserver - Pass statusItem to nativeMenu() for consistency (future-proofing) The context menu now follows the global system theme (light/dark), independent of the menu bar appearance setting. Fixes issue where tray context menu remained in dark mode when system was in light mode. --- maclib/tray.swift | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/maclib/tray.swift b/maclib/tray.swift index 1fba0e5..32252d0 100644 --- a/maclib/tray.swift +++ b/maclib/tray.swift @@ -113,6 +113,11 @@ private class MenuBarAppearanceObserver { if let img = isDark ? ctx.darkImage : ctx.lightImage { ctx.statusItem.button?.image = img } + + // Update menu appearance to match the system theme (not menu bar) + if let menu = ctx.contextMenu { + menu.appearance = systemAppearance() + } } // Cancel any pending settle callback before scheduling a new one. @@ -138,11 +143,22 @@ private class MenuBarAppearanceObserver { private var menuDelegate: MenuDelegate? // MARK: - Helpers -private func nativeMenu(from menuPtr: UnsafeMutableRawPointer) -> NSMenu { + +/// Returns the system-wide appearance (not the menu bar appearance) +private func systemAppearance() -> NSAppearance { + // Check if system is in dark mode via UserDefaults + let isDarkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" + let appearanceName: NSAppearance.Name = isDarkMode ? .darkAqua : .aqua + return NSAppearance(named: appearanceName) ?? NSApp.effectiveAppearance +} +private func nativeMenu(from menuPtr: UnsafeMutableRawPointer, statusItem: NSStatusItem? = nil) -> NSMenu { let menu = NSMenu() menu.autoenablesItems = false menu.delegate = menuDelegate + // Set menu appearance to match the system theme (not menu bar) + menu.appearance = systemAppearance() + var currentPtr = menuPtr while true { guard let textPtr = currentPtr.load(as: UnsafePointer?.self) else { break } @@ -179,7 +195,7 @@ private func nativeMenu(from menuPtr: UnsafeMutableRawPointer) -> NSMenu { menu.addItem(item) if let submenuPtr = submenu { - menu.setSubmenu(nativeMenu(from: submenuPtr), for: item) + menu.setSubmenu(nativeMenu(from: submenuPtr, statusItem: statusItem), for: item) } } @@ -269,7 +285,7 @@ public func tray_update(_ tray: UnsafeMutableRawPointer) { if let menuPtr = menuPtr { // Create and store the menu without assigning it to statusItem - ctx.contextMenu = nativeMenu(from: menuPtr) + ctx.contextMenu = nativeMenu(from: menuPtr, statusItem: statusItem) } else { ctx.contextMenu = nil } From 07275e8b2e021f4e688c29bccd273c8e03cd61d3 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Fri, 6 Feb 2026 10:41:37 +0200 Subject: [PATCH 2/3] Add dynamic system theme observer for context menu - Add DistributedNotificationCenter observer for AppleInterfaceThemeChangedNotification - Track system theme separately from menu bar appearance - Update menu appearance immediately when system theme changes - Remove observer properly in invalidate() The context menu now updates dynamically when the user changes the system theme in System Preferences, without requiring app restart. --- maclib/tray.swift | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/maclib/tray.swift b/maclib/tray.swift index 32252d0..ab599fb 100644 --- a/maclib/tray.swift +++ b/maclib/tray.swift @@ -72,6 +72,7 @@ private class MenuBarAppearanceObserver { private var workItem: DispatchWorkItem? private var settleItem: DispatchWorkItem? private var lastAppearance: NSAppearance.Name? + private var lastSystemTheme: Bool? // Track system theme separately private let trayPtr: UnsafeMutableRawPointer? /// Debounce delay before first evaluation (keep tiny but non‑zero). @@ -84,12 +85,41 @@ private class MenuBarAppearanceObserver { } func startObserving(_ statusItem: NSStatusItem) { + // Observe menu bar appearance changes observation = statusItem.button?.observe( \.effectiveAppearance, options: [.initial, .new] ) { [weak self] button, _ in self?.scheduleCheck(for: button.effectiveAppearance) } + + // Observe system-wide theme changes via DistributedNotificationCenter + DistributedNotificationCenter.default().addObserver( + forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"), + object: nil, + queue: .main + ) { [weak self] _ in + self?.handleSystemThemeChange() + } + + // Initial update of menu appearance + handleSystemThemeChange() + } + + private func handleSystemThemeChange() { + let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" + + // Only update if theme actually changed + if lastSystemTheme != isDark { + lastSystemTheme = isDark + + // Update menu appearance for all contexts + if let ptr = trayPtr, let ctx = contexts[ptr] { + if let menu = ctx.contextMenu { + menu.appearance = systemAppearance() + } + } + } } private func scheduleCheck(for appearance: NSAppearance) { @@ -136,6 +166,13 @@ private class MenuBarAppearanceObserver { observation = nil workItem?.cancel() settleItem?.cancel() + + // Remove system theme observer + DistributedNotificationCenter.default().removeObserver( + self, + name: NSNotification.Name("AppleInterfaceThemeChangedNotification"), + object: nil + ) } } From 9df769d8bc539d1f982744cc47bf4df1d521c528 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Fri, 6 Feb 2026 10:45:27 +0200 Subject: [PATCH 3/3] Fix submenus not syncing with main context menu appearance - Add updateMenuAppearance() helper to recursively update menu and all submenus - Replace direct appearance assignments with recursive updates - Ensures all submenu levels match the system theme Fixes issue where submenus remained in the wrong appearance when the system theme changed. --- maclib/tray.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/maclib/tray.swift b/maclib/tray.swift index ab599fb..bcd957e 100644 --- a/maclib/tray.swift +++ b/maclib/tray.swift @@ -116,7 +116,7 @@ private class MenuBarAppearanceObserver { // Update menu appearance for all contexts if let ptr = trayPtr, let ctx = contexts[ptr] { if let menu = ctx.contextMenu { - menu.appearance = systemAppearance() + updateMenuAppearance(menu, to: systemAppearance()) } } } @@ -146,7 +146,7 @@ private class MenuBarAppearanceObserver { // Update menu appearance to match the system theme (not menu bar) if let menu = ctx.contextMenu { - menu.appearance = systemAppearance() + updateMenuAppearance(menu, to: systemAppearance()) } } @@ -188,6 +188,18 @@ private func systemAppearance() -> NSAppearance { let appearanceName: NSAppearance.Name = isDarkMode ? .darkAqua : .aqua return NSAppearance(named: appearanceName) ?? NSApp.effectiveAppearance } + +/// Updates the appearance of a menu and all its submenus recursively +private func updateMenuAppearance(_ menu: NSMenu, to appearance: NSAppearance) { + menu.appearance = appearance + + // Recursively update all submenus + for item in menu.items { + if let submenu = item.submenu { + updateMenuAppearance(submenu, to: appearance) + } + } +} private func nativeMenu(from menuPtr: UnsafeMutableRawPointer, statusItem: NSStatusItem? = nil) -> NSMenu { let menu = NSMenu() menu.autoenablesItems = false