@@ -72,6 +72,7 @@ private class MenuBarAppearanceObserver {
7272 private var workItem : DispatchWorkItem ?
7373 private var settleItem : DispatchWorkItem ?
7474 private var lastAppearance : NSAppearance . Name ?
75+ private var lastSystemTheme : Bool ? // Track system theme separately
7576 private let trayPtr : UnsafeMutableRawPointer ?
7677
7778 /// Debounce delay before first evaluation (keep tiny but non‑zero).
@@ -84,12 +85,41 @@ private class MenuBarAppearanceObserver {
8485 }
8586
8687 func startObserving( _ statusItem: NSStatusItem ) {
88+ // Observe menu bar appearance changes
8789 observation = statusItem. button? . observe (
8890 \. effectiveAppearance,
8991 options: [ . initial, . new]
9092 ) { [ weak self] button, _ in
9193 self ? . scheduleCheck ( for: button. effectiveAppearance)
9294 }
95+
96+ // Observe system-wide theme changes via DistributedNotificationCenter
97+ DistributedNotificationCenter . default ( ) . addObserver (
98+ forName: NSNotification . Name ( " AppleInterfaceThemeChangedNotification " ) ,
99+ object: nil ,
100+ queue: . main
101+ ) { [ weak self] _ in
102+ self ? . handleSystemThemeChange ( )
103+ }
104+
105+ // Initial update of menu appearance
106+ handleSystemThemeChange ( )
107+ }
108+
109+ private func handleSystemThemeChange( ) {
110+ let isDark = UserDefaults . standard. string ( forKey: " AppleInterfaceStyle " ) == " Dark "
111+
112+ // Only update if theme actually changed
113+ if lastSystemTheme != isDark {
114+ lastSystemTheme = isDark
115+
116+ // Update menu appearance for all contexts
117+ if let ptr = trayPtr, let ctx = contexts [ ptr] {
118+ if let menu = ctx. contextMenu {
119+ updateMenuAppearance ( menu, to: systemAppearance ( ) )
120+ }
121+ }
122+ }
93123 }
94124
95125 private func scheduleCheck( for appearance: NSAppearance ) {
@@ -113,6 +143,11 @@ private class MenuBarAppearanceObserver {
113143 if let img = isDark ? ctx. darkImage : ctx. lightImage {
114144 ctx. statusItem. button? . image = img
115145 }
146+
147+ // Update menu appearance to match the system theme (not menu bar)
148+ if let menu = ctx. contextMenu {
149+ updateMenuAppearance ( menu, to: systemAppearance ( ) )
150+ }
116151 }
117152
118153 // Cancel any pending settle callback before scheduling a new one.
@@ -131,18 +166,48 @@ private class MenuBarAppearanceObserver {
131166 observation = nil
132167 workItem? . cancel ( )
133168 settleItem? . cancel ( )
169+
170+ // Remove system theme observer
171+ DistributedNotificationCenter . default ( ) . removeObserver (
172+ self ,
173+ name: NSNotification . Name ( " AppleInterfaceThemeChangedNotification " ) ,
174+ object: nil
175+ )
134176 }
135177}
136178
137179// MARK: - Globals that need to live for app lifetime
138180private var menuDelegate : MenuDelegate ?
139181
140182// MARK: - Helpers
141- private func nativeMenu( from menuPtr: UnsafeMutableRawPointer ) -> NSMenu {
183+
184+ /// Returns the system-wide appearance (not the menu bar appearance)
185+ private func systemAppearance( ) -> NSAppearance {
186+ // Check if system is in dark mode via UserDefaults
187+ let isDarkMode = UserDefaults . standard. string ( forKey: " AppleInterfaceStyle " ) == " Dark "
188+ let appearanceName : NSAppearance . Name = isDarkMode ? . darkAqua : . aqua
189+ return NSAppearance ( named: appearanceName) ?? NSApp . effectiveAppearance
190+ }
191+
192+ /// Updates the appearance of a menu and all its submenus recursively
193+ private func updateMenuAppearance( _ menu: NSMenu , to appearance: NSAppearance ) {
194+ menu. appearance = appearance
195+
196+ // Recursively update all submenus
197+ for item in menu. items {
198+ if let submenu = item. submenu {
199+ updateMenuAppearance ( submenu, to: appearance)
200+ }
201+ }
202+ }
203+ private func nativeMenu( from menuPtr: UnsafeMutableRawPointer , statusItem: NSStatusItem ? = nil ) -> NSMenu {
142204 let menu = NSMenu ( )
143205 menu. autoenablesItems = false
144206 menu. delegate = menuDelegate
145207
208+ // Set menu appearance to match the system theme (not menu bar)
209+ menu. appearance = systemAppearance ( )
210+
146211 var currentPtr = menuPtr
147212 while true {
148213 guard let textPtr = currentPtr. load ( as: UnsafePointer< CChar>? . self ) else { break }
@@ -179,7 +244,7 @@ private func nativeMenu(from menuPtr: UnsafeMutableRawPointer) -> NSMenu {
179244 menu. addItem ( item)
180245
181246 if let submenuPtr = submenu {
182- menu. setSubmenu ( nativeMenu ( from: submenuPtr) , for: item)
247+ menu. setSubmenu ( nativeMenu ( from: submenuPtr, statusItem : statusItem ) , for: item)
183248 }
184249 }
185250
@@ -269,7 +334,7 @@ public func tray_update(_ tray: UnsafeMutableRawPointer) {
269334
270335 if let menuPtr = menuPtr {
271336 // Create and store the menu without assigning it to statusItem
272- ctx. contextMenu = nativeMenu ( from: menuPtr)
337+ ctx. contextMenu = nativeMenu ( from: menuPtr, statusItem : statusItem )
273338 } else {
274339 ctx. contextMenu = nil
275340 }
0 commit comments