diff --git a/.gitignore b/.gitignore
index 8d394d2..96a6a4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,57 @@
.DS_Store
+# Xcode
+*.xcuserstate
+*.xcuserdata
+*.xcuserdatad
+*.pbxuser
+*.xccheckout
+*.xcplayground
+*.xcuserdatad
+*.xctest
+*.xcuserdata
+*.xcodeproj/xcshareddata/WorkspaceSettings.xcsettings
+
+# CocoaPods
+Pods/
+podfile.lock
+
+# Carthage
+Carthage/Build/
+
+# Swift Package Manager
+.swiftpm/
+.build/
+
+# Derived data
+DerivedData/
+
+# Build output
+build/
+*.app
+*.dSYM
+
+# Temporary files
+*.swp
+*.swo
+*.tmp
+*.log
+
+# Other
+*.DS_Store
+*.vscode/
+*.idea/
+
+# User-specific files
+*.xcuserdatad/
+*.xcodeproj/xcuserdata
+*.xcodeproj/project.xcuserdata
+
+# Build artifacts
+*.ipa
+*.xcarchive
+*.dSYM
+
+
# Xcode
#
build/
@@ -88,6 +141,7 @@ ONBOARDING_ENHANCEMENT.md
SHEET_PRESENTATION_FIX.md
lock_mode_reference.md
liquid_glass.md
+huds.md
MoveToSky.xcodeproj
/MoveToSky/
\ No newline at end of file
diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj
index 2a8b3eb..a587c08 100644
--- a/DynamicIsland.xcodeproj/project.pbxproj
+++ b/DynamicIsland.xcodeproj/project.pbxproj
@@ -379,12 +379,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = DynamicIsland/DynamicIsland.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\"";
- DEVELOPMENT_TEAM = 9Y64TRM77N;
+ DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -410,6 +411,7 @@
MARKETING_VERSION = "1.0.0-beta";
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
@@ -425,12 +427,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = DynamicIsland/DynamicIsland.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\"";
- DEVELOPMENT_TEAM = 9Y64TRM77N;
+ DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -456,6 +459,7 @@
MARKETING_VERSION = "1.0.0-beta";
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
diff --git a/DynamicIsland.xcodeproj/xcshareddata/xcschemes/DynamicIsland.xcscheme b/DynamicIsland.xcodeproj/xcshareddata/xcschemes/DynamicIsland.xcscheme
new file mode 100644
index 0000000..9b2fc4e
--- /dev/null
+++ b/DynamicIsland.xcodeproj/xcshareddata/xcschemes/DynamicIsland.xcscheme
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift
index 418f97e..a3adef5 100644
--- a/DynamicIsland/DynamicIslandApp.swift
+++ b/DynamicIsland/DynamicIslandApp.swift
@@ -93,10 +93,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var cancellables = Set()
private var windowsHiddenForLock = false
- // Media key monitoring
- private var globalEventMonitor: Any?
- private var localEventMonitor: Any?
-
// Debouncing mechanism for window size updates
private var windowSizeUpdateWorkItem: DispatchWorkItem?
// let calendarManager = CalendarManager.shared
@@ -127,84 +123,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return false
}
- // MARK: - Media Key Monitoring
-
- private func setupMediaKeyMonitoring() {
- // Monitor both global and local events to catch media keys
- globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .systemDefined) { [weak self] event in
- self?.handleSystemEvent(event)
- }
-
- localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .systemDefined) { [weak self] event in
- self?.handleSystemEvent(event)
- return event
- }
-
- print("πΉ Media key monitoring started using NSEvent")
- }
-
- private func handleSystemEvent(_ event: NSEvent) {
- guard event.subtype.rawValue == 8 else { return }
-
- let data1 = event.data1
- let keyCode = Int32((data1 & 0xFFFF0000) >> 16)
- let keyFlags = Int(data1 & 0x0000FFFF)
- let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA // KeyDown
- let keyRepeat = (keyFlags & 0x1) != 0
-
- guard keyState && !keyRepeat else { return } // Only handle key down, ignore repeats
-
- switch keyCode {
- case NX_KEYTYPE_SOUND_UP, NX_KEYTYPE_SOUND_DOWN, NX_KEYTYPE_MUTE:
- print("π Volume key detected: \(keyCode)")
- SystemHUDManager.shared.handleVolumeKeyEvent()
- case NX_KEYTYPE_BRIGHTNESS_UP, NX_KEYTYPE_BRIGHTNESS_DOWN:
- print("βοΈ Brightness key detected: \(keyCode)")
- SystemHUDManager.shared.handleBrightnessKeyEvent()
- default:
- break
- }
- }
-
- private func stopMediaKeyMonitoring() {
- if let globalMonitor = globalEventMonitor {
- NSEvent.removeMonitor(globalMonitor)
- globalEventMonitor = nil
- }
-
- if let localMonitor = localEventMonitor {
- NSEvent.removeMonitor(localMonitor)
- localEventMonitor = nil
- }
-
- print("πΉ Media key monitoring stopped")
- }
-
- // MARK: - MediaKeyApplicationDelegate (for future use)
-
- func didDetectVolumeKey() {
- SystemHUDManager.shared.handleVolumeKeyEvent()
- }
-
- func didDetectBrightnessKey() {
- SystemHUDManager.shared.handleBrightnessKeyEvent()
- }
-
func applicationWillTerminate(_ notification: Notification) {
// Cancel any pending window size updates
windowSizeUpdateWorkItem?.cancel()
-
- // Stop media key monitoring
- stopMediaKeyMonitoring()
-
- print("πͺ Application terminating - ensuring system HUD is restored...")
-
- // Re-enable system HUD on app termination
- SystemOSDManager.enableSystemHUD()
-
- // Give it time to complete
- usleep(500000) // 500ms
-
NotificationCenter.default.removeObserver(self)
}
@@ -454,9 +375,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Setup Privacy Indicator Manager (camera and microphone monitoring)
PrivacyIndicatorManager.shared.startMonitoring()
- // Setup media key monitoring
- setupMediaKeyMonitoring()
-
// Observe tab changes - use debounced updates
coordinator.$currentView.sink { [weak self] newView in
self?.debouncedUpdateWindowSize()
diff --git a/DynamicIsland/managers/MediaKeyInterceptor.swift b/DynamicIsland/managers/MediaKeyInterceptor.swift
new file mode 100644
index 0000000..2821769
--- /dev/null
+++ b/DynamicIsland/managers/MediaKeyInterceptor.swift
@@ -0,0 +1,204 @@
+import Foundation
+import AppKit
+import CoreGraphics
+#if canImport(ApplicationServices)
+import ApplicationServices
+#endif
+
+private let NX_SYSDEFINED_EVENT_TYPE: UInt32 = 14
+private let NX_KEYTYPE_SOUND_UP: Int32 = 0
+private let NX_KEYTYPE_SOUND_DOWN: Int32 = 1
+private let NX_KEYTYPE_BRIGHTNESS_UP: Int32 = 2
+private let NX_KEYTYPE_BRIGHTNESS_DOWN: Int32 = 3
+private let NX_KEYTYPE_MUTE: Int32 = 7
+
+enum MediaKeyDirection {
+ case up
+ case down
+}
+
+struct MediaKeyConfiguration {
+ var interceptVolume: Bool
+ var interceptBrightness: Bool
+
+ static let disabled = MediaKeyConfiguration(interceptVolume: false, interceptBrightness: false)
+}
+
+protocol MediaKeyInterceptorDelegate: AnyObject {
+ func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, isRepeat: Bool)
+ func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, isRepeat: Bool)
+ func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor)
+}
+
+final class MediaKeyInterceptor {
+ static let shared = MediaKeyInterceptor()
+
+ weak var delegate: MediaKeyInterceptorDelegate?
+ var configuration: MediaKeyConfiguration = .disabled {
+ didSet {
+ updateTapState()
+ }
+ }
+
+ private var eventTap: CFMachPort?
+ private var runLoopSource: CFRunLoopSource?
+ private var isTapEnabled = false
+#if canImport(ApplicationServices)
+ private var didRequestAccessibilityPrompt = false
+#endif
+ private let systemDefinedEventType = CGEventType(rawValue: NX_SYSDEFINED_EVENT_TYPE)
+ private let eventTapLocations: [CGEventTapLocation] = [.cghidEventTap, .cgSessionEventTap]
+
+ private init() {}
+
+ @discardableResult
+ func start() -> Bool {
+ guard eventTap == nil else {
+ updateTapState()
+ return true
+ }
+
+#if canImport(ApplicationServices)
+ requestAccessibilityPermissionIfNeeded()
+#endif
+
+ guard let systemDefinedType = systemDefinedEventType else {
+ NSLog("β Unable to resolve system-defined event type")
+ return false
+ }
+ let mask = CGEventMask(1) << systemDefinedType.rawValue
+ let callback: CGEventTapCallBack = { _, type, cgEvent, userInfo in
+ guard let userInfo else { return Unmanaged.passUnretained(cgEvent) }
+ let interceptor = Unmanaged.fromOpaque(userInfo).takeUnretainedValue()
+ return interceptor.handleEvent(cgEvent: cgEvent, type: type)
+ }
+
+ var createdTap: CFMachPort?
+ for location in eventTapLocations {
+ if let tap = CGEvent.tapCreate(
+ tap: location,
+ place: .headInsertEventTap,
+ options: .defaultTap,
+ eventsOfInterest: mask,
+ callback: callback,
+ userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
+ ) {
+ createdTap = tap
+ break
+ }
+ }
+
+ guard let tap = createdTap else {
+#if canImport(ApplicationServices)
+ if !AXIsProcessTrusted() {
+ NSLog("β οΈ Accessibility permission missing; grant access in System Settings βΊ Privacy & Security βΊ Accessibility")
+ }
+#endif
+ NSLog("β Failed to create media key event tap")
+ return false
+ }
+
+ eventTap = tap
+ runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
+ if let source = runLoopSource {
+ CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
+ }
+ CGEvent.tapEnable(tap: tap, enable: true)
+ isTapEnabled = true
+ NSLog("β
Media key event tap installed (HID)")
+ return true
+ }
+
+ func stop() {
+ if let tap = eventTap {
+ CGEvent.tapEnable(tap: tap, enable: false)
+ CFMachPortInvalidate(tap)
+ }
+ if let source = runLoopSource {
+ CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes)
+ }
+ eventTap = nil
+ runLoopSource = nil
+ isTapEnabled = false
+ }
+
+ private func updateTapState() {
+ guard let tap = eventTap else { return }
+ let shouldEnable = configuration.interceptVolume || configuration.interceptBrightness
+ if shouldEnable != isTapEnabled {
+ CGEvent.tapEnable(tap: tap, enable: shouldEnable)
+ isTapEnabled = shouldEnable
+ }
+ }
+
+ private func handleEvent(cgEvent: CGEvent, type: CGEventType) -> Unmanaged? {
+ guard let systemDefinedType = systemDefinedEventType,
+ type == systemDefinedType,
+ let nsEvent = NSEvent(cgEvent: cgEvent),
+ nsEvent.subtype.rawValue == 8 else {
+ return Unmanaged.passUnretained(cgEvent)
+ }
+
+ let data1 = nsEvent.data1
+ let keyCode = (data1 & 0xFFFF0000) >> 16
+ let keyFlags = data1 & 0x0000FFFF
+ let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA // 0xA = keyDown, 0xB = keyUp
+ let isRepeat = (keyFlags & 0x0001) == 1
+
+ guard keyState else {
+ // Swallow key-up events only when intercepting, otherwise let them pass through
+ if shouldHandle(keyCode: Int32(keyCode)) {
+ return nil
+ }
+ return Unmanaged.passUnretained(cgEvent)
+ }
+
+ switch Int32(keyCode) {
+ case NX_KEYTYPE_SOUND_UP:
+ guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
+ delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .up, isRepeat: isRepeat)
+ return nil
+ case NX_KEYTYPE_SOUND_DOWN:
+ guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
+ delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .down, isRepeat: isRepeat)
+ return nil
+ case NX_KEYTYPE_MUTE:
+ guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
+ delegate?.mediaKeyInterceptorDidToggleMute(self)
+ return nil
+ case NX_KEYTYPE_BRIGHTNESS_UP:
+ guard configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
+ delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .up, isRepeat: isRepeat)
+ return nil
+ case NX_KEYTYPE_BRIGHTNESS_DOWN:
+ guard configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
+ delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .down, isRepeat: isRepeat)
+ return nil
+ default:
+ return Unmanaged.passUnretained(cgEvent)
+ }
+ }
+
+ private func shouldHandle(keyCode: Int32) -> Bool {
+ switch keyCode {
+ case NX_KEYTYPE_SOUND_UP, NX_KEYTYPE_SOUND_DOWN, NX_KEYTYPE_MUTE:
+ return configuration.interceptVolume
+ case NX_KEYTYPE_BRIGHTNESS_UP, NX_KEYTYPE_BRIGHTNESS_DOWN:
+ return configuration.interceptBrightness
+ default:
+ return false
+ }
+ }
+}
+
+#if canImport(ApplicationServices)
+extension MediaKeyInterceptor {
+ private func requestAccessibilityPermissionIfNeeded() {
+ guard !AXIsProcessTrusted(), !didRequestAccessibilityPrompt else { return }
+ let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
+ let options: CFDictionary = [promptKey: true] as CFDictionary
+ _ = AXIsProcessTrustedWithOptions(options)
+ didRequestAccessibilityPrompt = true
+ }
+}
+#endif
diff --git a/DynamicIsland/managers/SystemChangesObserver.swift b/DynamicIsland/managers/SystemChangesObserver.swift
index a609214..8e1ca19 100644
--- a/DynamicIsland/managers/SystemChangesObserver.swift
+++ b/DynamicIsland/managers/SystemChangesObserver.swift
@@ -1,347 +1,97 @@
-//
-// SystemChangesObserver.swift
-// DynamicIsland
-//
-// Adapted from TheBoringWorker-HUD ChangesObserver.swift
-// Created by GitHub Copilot on 06/09/25.
-//
-
-import Cocoa
import Foundation
+import CoreGraphics
import Defaults
-import CoreAudio
-class SystemChangesObserver {
- private var oldVolume: Float = 0
- private var oldMuted: Bool = false
- private var oldBrightness: Float = 0
-
+final class SystemChangesObserver: MediaKeyInterceptorDelegate {
private weak var coordinator: DynamicIslandViewCoordinator?
-
- // Adaptive polling timer
- private var pollingTimer: Timer?
- private var currentPollingInterval: TimeInterval = 2.0
-
- // Polling intervals
- private let rapidPollingInterval: TimeInterval = 0.05 // Burst mode for active changes
- private let normalPollingInterval: TimeInterval = 2.0 // Normal mode when idle
-
- // Activity tracking
- private var lastBrightnessChangeTime: Date = Date.distantPast
- private var lastVolumeChangeTime: Date = Date.distantPast
- private var lastVolumeCheckTime: Date = Date.distantPast
- private var lastExternalChangeTime: Date = Date.distantPast // Track external volume changes
-
- // Headphone detection (for info only, doesn't affect polling)
- private var hasHeadphonesConnected: Bool = false
+ private let volumeController = SystemVolumeController.shared
+ private let brightnessController = SystemBrightnessController.shared
+ private let mediaKeyInterceptor = MediaKeyInterceptor.shared
+
+ private let volumeStep: Float = 1.0 / 16.0
+ private let brightnessStep: Float = 1.0 / 16.0
+
+ private var volumeEnabled = false
+ private var brightnessEnabled = false
init(coordinator: DynamicIslandViewCoordinator) {
self.coordinator = coordinator
-
- // Initialize baseline values only once
- oldVolume = SystemVolumeManager.getOutputVolume()
- oldMuted = SystemVolumeManager.isMuted()
-
- do {
- oldBrightness = try SystemDisplayManager.getDisplayBrightness()
- } catch {
- NSLog("Failed to retrieve initial display brightness: \(error)")
- }
-
- // Detect headphones on init
- updateHeadphoneStatus()
- setupAudioRouteChangeListener()
}
- func startObserving() {
- guard Defaults[.enableSystemHUD] else { return }
-
- // Start with appropriate polling rate
- startPollingTimer(interval: determinePollingInterval())
- }
-
- private func determinePollingInterval() -> TimeInterval {
- let now = Date()
-
- // Burst mode: Rapid polling within 5 seconds of any activity
- // Activity = key press OR external change detected
- let timeSinceKeyPress = min(
- now.timeIntervalSince(lastVolumeChangeTime),
- now.timeIntervalSince(lastBrightnessChangeTime)
+ func startObserving(volumeEnabled: Bool, brightnessEnabled: Bool) {
+ self.volumeEnabled = volumeEnabled
+ self.brightnessEnabled = brightnessEnabled
+
+ volumeController.onVolumeChange = { [weak self] volume, muted in
+ guard let self, self.volumeEnabled else { return }
+ let value = muted ? 0 : volume
+ self.sendVolumeNotification(value: value)
+ }
+ volumeController.onRouteChange = { [weak self] in
+ guard let self, self.volumeEnabled else { return }
+ self.sendVolumeNotification(value: self.volumeController.isMuted ? 0 : self.volumeController.currentVolume)
+ }
+ volumeController.start()
+
+ brightnessController.onBrightnessChange = { [weak self] brightness in
+ guard let self, self.brightnessEnabled else { return }
+ self.sendBrightnessNotification(value: brightness)
+ }
+ brightnessController.start()
+
+ mediaKeyInterceptor.delegate = self
+ let tapStarted = mediaKeyInterceptor.start()
+ if !tapStarted {
+ NSLog("β οΈ Media key interception unavailable; system HUD will remain visible")
+ }
+ mediaKeyInterceptor.configuration = MediaKeyConfiguration(
+ interceptVolume: volumeEnabled,
+ interceptBrightness: brightnessEnabled
)
- let timeSinceExternalChange = now.timeIntervalSince(lastExternalChangeTime)
-
- // Use burst mode if there's been recent activity (within 5 seconds)
- if timeSinceKeyPress < 5.0 || timeSinceExternalChange < 5.0 {
- return rapidPollingInterval
- }
-
- // Normal mode: Idle state (regardless of headphones)
- return normalPollingInterval
- }
-
- private func startPollingTimer(interval: TimeInterval) {
- // Don't restart if same interval
- if pollingTimer != nil && currentPollingInterval == interval {
- return
- }
-
- currentPollingInterval = interval
- pollingTimer?.invalidate()
-
- pollingTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
- guard let self = self, Defaults[.enableSystemHUD] else { return }
-
- // Check if we need to adjust polling rate
- let newInterval = self.determinePollingInterval()
- if newInterval != self.currentPollingInterval {
- self.startPollingTimer(interval: newInterval)
- return
- }
-
- // Poll volume
- self.checkVolumeChanges()
-
- // Poll brightness only within 5 seconds of key press
- if Date().timeIntervalSince(self.lastBrightnessChangeTime) < 5.0 {
- self.checkBrightnessChanges()
- }
- }
-
- let mode = interval == rapidPollingInterval ? "BURST (50ms)" : "NORMAL (2s)"
- print("β
Polling started: \(mode) mode\(hasHeadphonesConnected ? " [π§ Headphones connected]" : "")")
- }
-
- // MARK: - Headphone Detection
-
- private func setupAudioRouteChangeListener() {
- // Listen for default audio device changes
- var propertyAddress = AudioObjectPropertyAddress(
- mSelector: kAudioHardwarePropertyDefaultOutputDevice,
- mScope: kAudioObjectPropertyScopeGlobal,
- mElement: kAudioObjectPropertyElementMain
- )
-
- AudioObjectAddPropertyListener(
- AudioObjectID(kAudioObjectSystemObject),
- &propertyAddress,
- audioDeviceChangeListener,
- Unmanaged.passUnretained(self).toOpaque()
- )
- }
-
- @objc fileprivate func audioRouteChanged() {
- updateHeadphoneStatus()
-
- // Adjust polling rate immediately
- startPollingTimer(interval: determinePollingInterval())
- }
-
- private func updateHeadphoneStatus() {
- // Check for headphones/external audio devices
- var propertyAddress = AudioObjectPropertyAddress(
- mSelector: kAudioHardwarePropertyDefaultOutputDevice,
- mScope: kAudioObjectPropertyScopeGlobal,
- mElement: kAudioObjectPropertyElementMain
- )
-
- var deviceID: AudioDeviceID = 0
- var propertySize = UInt32(MemoryLayout.size)
-
- let status = AudioObjectGetPropertyData(
- AudioObjectID(kAudioObjectSystemObject),
- &propertyAddress,
- 0,
- nil,
- &propertySize,
- &deviceID
- )
-
- guard status == noErr else {
- hasHeadphonesConnected = false
- return
- }
-
- // Check if device is not built-in speakers
- var transportType: UInt32 = 0
- var transportSize = UInt32(MemoryLayout.size)
- var transportAddress = AudioObjectPropertyAddress(
- mSelector: kAudioDevicePropertyTransportType,
- mScope: kAudioObjectPropertyScopeGlobal,
- mElement: kAudioObjectPropertyElementMain
- )
-
- let transportStatus = AudioObjectGetPropertyData(
- deviceID,
- &transportAddress,
- 0,
- nil,
- &transportSize,
- &transportType
- )
-
- // Consider headphones if: Bluetooth, USB, or any non-built-in device
- let wasConnected = hasHeadphonesConnected
- hasHeadphonesConnected = transportStatus == noErr &&
- transportType != kAudioDeviceTransportTypeBuiltIn
-
- if wasConnected != hasHeadphonesConnected {
- print(hasHeadphonesConnected ? "π§ Headphones connected (will use burst mode on change detection)" :
- "π Headphones disconnected")
- }
- }
-
- // MARK: - Public Media Key Event Handlers (called from MediaKeyApplication)
-
- func handleVolumeKeyEvent() {
- // Mark as user-initiated and trigger burst mode
- lastVolumeChangeTime = Date()
-
- // Switch to burst mode immediately
- startPollingTimer(interval: rapidPollingInterval)
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
- self.handleVolumeKeyPress()
- }
- }
-
- func handleBrightnessKeyEvent() {
- // Mark as user-initiated and trigger burst mode
- lastBrightnessChangeTime = Date()
-
- // Switch to burst mode immediately
- startPollingTimer(interval: rapidPollingInterval)
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
- self.handleBrightnessKeyPress()
- }
- }
-
- // MARK: - Key Press Event Handlers
-
- private func handleVolumeKeyPress() {
- guard Defaults[.enableVolumeHUD] else { return }
-
- // Mark as user-initiated (don't debounce key presses)
- lastVolumeChangeTime = Date()
-
- let newVolume = SystemVolumeManager.getOutputVolume()
- let newMuted = SystemVolumeManager.isMuted()
-
- // Always show HUD for key press feedback, even if value doesn't change
- print("π Volume key pressed - showing HUD: volume=\(newVolume), muted=\(newMuted)")
-
- if newMuted {
- sendVolumeNotification(value: 0.0)
- } else {
- sendVolumeNotification(value: newVolume)
- }
-
- // Update stored values
- oldVolume = newVolume
- oldMuted = newMuted
- }
-
- private func handleBrightnessKeyPress() {
- guard Defaults[.enableBrightnessHUD] else { return }
- guard NSScreen.screens.count > 0 else { return }
-
- do {
- let newBrightness = try SystemDisplayManager.getDisplayBrightness()
-
- // Always show HUD for key press feedback, even if value doesn't change
- print("βοΈ Brightness key pressed - showing HUD: brightness=\(newBrightness) [BURST MODE]")
- sendBrightnessNotification(value: newBrightness)
-
- // Update stored value
- oldBrightness = newBrightness
- } catch {
- // Silently ignore brightness errors to reduce log spam
- }
- }
-
- // MARK: - Polling Check Methods
-
- private func checkVolumeChanges() {
- guard Defaults[.enableVolumeHUD] else { return }
-
- // Throttle AppleScript checks based on current mode
- let now = Date()
- let minInterval: TimeInterval = currentPollingInterval == rapidPollingInterval ? 0.05 : 0.2
-
- if now.timeIntervalSince(lastVolumeCheckTime) < minInterval {
- return
- }
- lastVolumeCheckTime = now
-
- let newVolume = SystemVolumeManager.getOutputVolume()
- let newMuted = SystemVolumeManager.isMuted()
-
- let volumeChanged = !isAlmost(firstNumber: oldVolume, secondNumber: newVolume)
- let muteChanged = newMuted != oldMuted
-
- if volumeChanged || muteChanged {
- // Check if this is an external change (not from key press)
- let debounceWindow = currentPollingInterval == rapidPollingInterval ? 0.1 : 0.5
- let timeSinceKeyPress = now.timeIntervalSince(lastVolumeChangeTime)
-
- // External change detected!
- if timeSinceKeyPress > debounceWindow {
- // Mark it and trigger burst mode
- lastExternalChangeTime = now
-
- let mode = currentPollingInterval == rapidPollingInterval ? "[BURST]" : "[EXTERNALβBURST]"
- print("π Volume change detected \(mode): \(oldVolume) β \(newVolume), muted: \(oldMuted) β \(newMuted)")
-
- // Show HUD immediately
- if newMuted {
- sendVolumeNotification(value: 0.0)
- } else {
- sendVolumeNotification(value: newVolume)
- }
-
- // Switch to burst mode immediately if we're in normal mode
- if currentPollingInterval == normalPollingInterval {
- print("β‘ Switching from NORMAL to BURST mode due to external change")
- startPollingTimer(interval: rapidPollingInterval)
- }
- } else {
- // Within debounce window of key press - still update values but don't show HUD
- print("π Volume change within debounce window (\(timeSinceKeyPress)s) - skipping HUD")
- }
-
- oldVolume = newVolume
- oldMuted = newMuted
- }
}
- private func checkBrightnessChanges() {
- guard Defaults[.enableBrightnessHUD] else { return }
- guard NSScreen.screens.count > 0 else { return }
-
- do {
- let newBrightness = try SystemDisplayManager.getDisplayBrightness()
- let brightnessChanged = !isAlmost(firstNumber: oldBrightness, secondNumber: newBrightness)
-
- if brightnessChanged {
- print("βοΈ Brightness change detected: \(oldBrightness) β \(newBrightness)")
- sendBrightnessNotification(value: newBrightness)
- oldBrightness = newBrightness
- }
- } catch {
- // Silently ignore brightness errors to reduce log spam
- }
+ func update(volumeEnabled: Bool, brightnessEnabled: Bool) {
+ self.volumeEnabled = volumeEnabled
+ self.brightnessEnabled = brightnessEnabled
+ mediaKeyInterceptor.configuration = MediaKeyConfiguration(
+ interceptVolume: volumeEnabled,
+ interceptBrightness: brightnessEnabled
+ )
}
-
- private func isAlmost(firstNumber: Float, secondNumber: Float) -> Bool {
- // Use a fixed sensitive threshold for volume changes
- // 0.015 = 1.5% change required (sensitive enough for headphone increments)
- let threshold: Float = 0.015
- return abs(firstNumber - secondNumber) <= threshold
+
+ func stopObserving() {
+ mediaKeyInterceptor.stop()
+ mediaKeyInterceptor.delegate = nil
+
+ volumeController.stop()
+ volumeController.onVolumeChange = nil
+ volumeController.onRouteChange = nil
+
+ brightnessController.stop()
+ brightnessController.onBrightnessChange = nil
}
-
- // MARK: - Internal Notification Methods
-
+
+ // MARK: - MediaKeyInterceptorDelegate
+
+ func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, isRepeat: Bool) {
+ guard volumeEnabled else { return }
+ let delta = (direction == .up ? volumeStep : -volumeStep)
+ volumeController.adjust(by: delta)
+ }
+
+ func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor) {
+ guard volumeEnabled else { return }
+ volumeController.toggleMute()
+ }
+
+ func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, isRepeat: Bool) {
+ guard brightnessEnabled else { return }
+ let delta = (direction == .up ? brightnessStep : -brightnessStep)
+ brightnessController.adjust(by: delta)
+ }
+
+ // MARK: - HUD Dispatch
+
private func sendVolumeNotification(value: Float) {
coordinator?.toggleSneakPeek(
status: true,
@@ -350,7 +100,7 @@ class SystemChangesObserver {
icon: ""
)
}
-
+
private func sendBrightnessNotification(value: Float) {
coordinator?.toggleSneakPeek(
status: true,
@@ -359,52 +109,6 @@ class SystemChangesObserver {
icon: ""
)
}
-
- deinit {
- // Remove audio device change listener
- var propertyAddress = AudioObjectPropertyAddress(
- mSelector: kAudioHardwarePropertyDefaultOutputDevice,
- mScope: kAudioObjectPropertyScopeGlobal,
- mElement: kAudioObjectPropertyElementMain
- )
-
- AudioObjectRemovePropertyListener(
- AudioObjectID(kAudioObjectSystemObject),
- &propertyAddress,
- audioDeviceChangeListener,
- Unmanaged.passUnretained(self).toOpaque()
- )
-
- stopObserving()
- }
-
- func stopObserving() {
- // Clean up polling timer
- pollingTimer?.invalidate()
- pollingTimer = nil
-
- print("πΉ Media key detection stopped")
- }
-}
-
-// MARK: - CoreAudio Callback for Device Changes
-
-private func audioDeviceChangeListener(
- _ inObjectID: AudioObjectID,
- _ inNumberAddresses: UInt32,
- _ inAddresses: UnsafePointer,
- _ inClientData: UnsafeMutableRawPointer?
-) -> OSStatus {
- guard let inClientData = inClientData else { return noErr }
-
- let observer = Unmanaged.fromOpaque(inClientData).takeUnretainedValue()
-
- // Handle on main thread
- DispatchQueue.main.async {
- observer.audioRouteChanged()
- }
-
- return noErr
}
diff --git a/DynamicIsland/managers/SystemHUDManager.swift b/DynamicIsland/managers/SystemHUDManager.swift
index cbd9c4f..dc64d38 100644
--- a/DynamicIsland/managers/SystemHUDManager.swift
+++ b/DynamicIsland/managers/SystemHUDManager.swift
@@ -8,7 +8,6 @@
import Foundation
import Defaults
import Combine
-import AppKit
class SystemHUDManager {
static let shared = SystemHUDManager()
@@ -43,20 +42,20 @@ class SystemHUDManager {
guard let self = self, self.isSetupComplete, Defaults[.enableSystemHUD] else {
return
}
- // When volume HUD is enabled, ensure OSD is disabled
- if change.newValue {
- self.disableSystemHUD()
- }
+ self.changesObserver?.update(
+ volumeEnabled: change.newValue,
+ brightnessEnabled: Defaults[.enableBrightnessHUD]
+ )
}.store(in: &cancellables)
Defaults.publisher(.enableBrightnessHUD, options: []).sink { [weak self] change in
guard let self = self, self.isSetupComplete, Defaults[.enableSystemHUD] else {
return
}
- // When brightness HUD is enabled, ensure OSD is disabled
- if change.newValue {
- self.disableSystemHUD()
- }
+ self.changesObserver?.update(
+ volumeEnabled: Defaults[.enableVolumeHUD],
+ brightnessEnabled: change.newValue
+ )
}.store(in: &cancellables)
}
@@ -88,13 +87,9 @@ class SystemHUDManager {
await stopSystemHUD() // Stop any existing observer
changesObserver = SystemChangesObserver(coordinator: coordinator)
- changesObserver?.startObserving()
-
- // Disable system HUD if possible
- disableSystemHUD()
-
- // Start event-driven OSD monitoring
- startOSDMonitoring()
+ let volumeEnabled = Defaults[.enableVolumeHUD]
+ let brightnessEnabled = Defaults[.enableBrightnessHUD]
+ changesObserver?.startObserving(volumeEnabled: volumeEnabled, brightnessEnabled: brightnessEnabled)
print("System HUD replacement started")
isSystemOperationInProgress = false
@@ -108,120 +103,14 @@ class SystemHUDManager {
changesObserver?.stopObserving()
changesObserver = nil
- // Stop OSD monitoring
- stopOSDMonitoring()
-
- // Re-enable system HUD
- enableOriginalSystemHUD()
-
print("System HUD replacement stopped")
isSystemOperationInProgress = false
}
- private func disableSystemHUD() {
- print("π Disabling system HUD (OSDUIHelper running: \(SystemOSDManager.isOSDUIHelperRunning()))")
- // Disable the system HUD using SystemOSDManager
- SystemOSDManager.disableSystemHUD()
- }
-
- private func enableOriginalSystemHUD() {
- print("π Re-enabling system HUD (OSDUIHelper running: \(SystemOSDManager.isOSDUIHelperRunning()))")
- // Re-enable the system HUD using SystemOSDManager
- SystemOSDManager.enableSystemHUD()
-
- // Check status after a reasonable delay, using async to avoid blocking
- Task {
- try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds instead of 1
- let isRunning = await SystemOSDManager.isOSDUIHelperRunningAsync()
- print("π OSDUIHelper status after re-enable: \(isRunning ? "β
Running" : "β Not running")")
- }
- }
-
- // MARK: - Media Key Event Handling
-
- /// Handle volume key press events from MediaKeyApplication
- public func handleVolumeKeyEvent() {
- changesObserver?.handleVolumeKeyEvent()
- }
-
- /// Handle brightness key press events from MediaKeyApplication
- public func handleBrightnessKeyEvent() {
- changesObserver?.handleBrightnessKeyEvent()
- }
-
- // MARK: - OSD Monitoring
-
- /// Start event-driven monitoring for OSDUIHelper process launches
- @MainActor
- private func startOSDMonitoring() {
- stopOSDMonitoring() // Stop any existing observer
-
- // Listen for process launch notifications
- NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didLaunchApplicationNotification)
- .compactMap { notification -> NSRunningApplication? in
- notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
- }
- .filter { app in
- // Check if OSDUIHelper was launched
- app.bundleIdentifier == "com.apple.OSDUIHelper" ||
- app.localizedName?.contains("OSDUIHelper") == true
- }
- .sink { [weak self] _ in
- guard let self = self else { return }
-
- // Check if any HUD is enabled
- let anyHUDEnabled = Defaults[.enableVolumeHUD] || Defaults[.enableBrightnessHUD]
- guard anyHUDEnabled else { return }
-
- Task { @MainActor in
- print("β οΈ OSDUIHelper launched - re-disabling")
- self.disableSystemHUD()
- }
- }
- .store(in: &cancellables)
-
- // Also listen for screen wake events (OSDUIHelper often restarts after wake)
- NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.screensDidWakeNotification)
- .sink { [weak self] _ in
- guard let self = self else { return }
-
- let anyHUDEnabled = Defaults[.enableVolumeHUD] || Defaults[.enableBrightnessHUD]
- guard anyHUDEnabled else { return }
-
- // Check after a short delay to ensure system is ready
- Task { @MainActor in
- try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
-
- let isRunning = await SystemOSDManager.isOSDUIHelperRunningAsync()
- if isRunning {
- print("β οΈ OSDUIHelper detected running after wake - re-disabling")
- self.disableSystemHUD()
- }
- }
- }
- .store(in: &cancellables)
-
- print("β
OSD monitoring started (event-driven)")
- }
-
- /// Stop OSD monitoring
- @MainActor
- private func stopOSDMonitoring() {
- // Cancellables are automatically cleaned up
- print("π OSD monitoring stopped")
- }
-
deinit {
cancellables.removeAll()
Task { @MainActor in
await stopSystemHUD()
}
- // Emergency restore is available via SystemHUDManager.emergencyRestoreSystemHUD() if needed
- }
-
- /// Emergency restore function - call this if system HUD is not working
- public static func emergencyRestoreSystemHUD() {
- print("π¨ Emergency system HUD restore initiated")
- SystemHUDDebugger.forceRestartOSDUIHelper()
}
}
\ No newline at end of file
diff --git a/DynamicIsland/managers/SystemMediaControllers.swift b/DynamicIsland/managers/SystemMediaControllers.swift
new file mode 100644
index 0000000..a5add30
--- /dev/null
+++ b/DynamicIsland/managers/SystemMediaControllers.swift
@@ -0,0 +1,397 @@
+import Foundation
+import CoreAudio
+import CoreGraphics
+import IOKit
+
+extension Notification.Name {
+ static let systemVolumeDidChange = Notification.Name("DynamicIsland.systemVolumeDidChange")
+ static let systemBrightnessDidChange = Notification.Name("DynamicIsland.systemBrightnessDidChange")
+ static let systemAudioRouteDidChange = Notification.Name("DynamicIsland.systemAudioRouteDidChange")
+}
+
+final class SystemVolumeController {
+ static let shared = SystemVolumeController()
+
+ var onVolumeChange: ((Float, Bool) -> Void)?
+ var onRouteChange: (() -> Void)?
+
+ private let callbackQueue = DispatchQueue(label: "com.dynamicisland.volume-listener")
+ private var currentDeviceID: AudioDeviceID = 0
+ private var listenersInstalled = false
+ private var volumeElement: AudioObjectPropertyElement?
+ private var muteElement: AudioObjectPropertyElement?
+
+ private let candidateElements: [AudioObjectPropertyElement] = [
+ kAudioObjectPropertyElementMain,
+ AudioObjectPropertyElement(1),
+ AudioObjectPropertyElement(2)
+ ]
+
+ private init() {
+ currentDeviceID = resolveDefaultDevice()
+ refreshPropertyElements()
+ installDefaultDeviceListener()
+ installVolumeListeners(for: currentDeviceID)
+ notifyCurrentState()
+ }
+
+ func start() {
+ // Listeners are installed during init, nothing else required.
+ }
+
+ func stop() {
+ // We keep listeners alive for the app lifetime; clearing closures prevents UI updates.
+ onVolumeChange = nil
+ onRouteChange = nil
+ }
+
+ func adjust(by delta: Float) {
+ var newValue = currentVolume + delta
+ newValue = max(0, min(1, newValue))
+ setVolume(newValue)
+ }
+
+ func toggleMute() {
+ setMuted(!isMuted)
+ }
+
+ var currentVolume: Float {
+ getVolume()
+ }
+
+ var isMuted: Bool {
+ getMuteState()
+ }
+
+ func setVolume(_ value: Float) {
+ var volume = max(0, min(1, value))
+ let status = setData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
+ if status != noErr {
+ NSLog("β οΈ Failed to set volume: \(status)")
+ }
+ }
+
+ func setMuted(_ muted: Bool) {
+ var muteFlag: UInt32 = muted ? 1 : 0
+ let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag)
+ if status != noErr {
+ NSLog("β οΈ Failed to set mute state: \(status)")
+ }
+ }
+
+ // MARK: - Private
+
+ private func resolveDefaultDevice() -> AudioDeviceID {
+ var deviceID = AudioDeviceID()
+ var size = UInt32(MemoryLayout.size(ofValue: deviceID))
+ var address = AudioObjectPropertyAddress(
+ mSelector: kAudioHardwarePropertyDefaultOutputDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMain
+ )
+ let status = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &deviceID)
+ if status != noErr {
+ NSLog("β οΈ Unable to fetch default audio device: \(status)")
+ }
+ return deviceID
+ }
+
+ private func installDefaultDeviceListener() {
+ guard !listenersInstalled else { return }
+ var address = AudioObjectPropertyAddress(
+ mSelector: kAudioHardwarePropertyDefaultOutputDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMain
+ )
+ let status = AudioObjectAddPropertyListenerBlock(
+ AudioObjectID(kAudioObjectSystemObject),
+ &address,
+ callbackQueue
+ ) { [weak self] _, _ in
+ guard let self else { return }
+ self.handleDefaultDeviceChanged()
+ }
+ if status != noErr {
+ NSLog("β οΈ Failed to install default device listener: \(status)")
+ }
+ listenersInstalled = true
+ }
+
+ private func installVolumeListeners(for deviceID: AudioDeviceID) {
+ if let element = resolveElement(selector: kAudioDevicePropertyVolumeScalar, deviceID: deviceID) {
+ volumeElement = element
+ var address = makeAddress(selector: kAudioDevicePropertyVolumeScalar, element: element)
+ AudioObjectAddPropertyListenerBlock(deviceID, &address, callbackQueue) { [weak self] _, _ in
+ self?.notifyCurrentState()
+ }
+ }
+
+ if let element = resolveElement(selector: kAudioDevicePropertyMute, deviceID: deviceID) {
+ muteElement = element
+ var address = makeAddress(selector: kAudioDevicePropertyMute, element: element)
+ AudioObjectAddPropertyListenerBlock(deviceID, &address, callbackQueue) { [weak self] _, _ in
+ self?.notifyCurrentState()
+ }
+ }
+ }
+
+ private func handleDefaultDeviceChanged() {
+ callbackQueue.async { [weak self] in
+ guard let self else { return }
+ self.currentDeviceID = self.resolveDefaultDevice()
+ self.refreshPropertyElements()
+ self.installVolumeListeners(for: self.currentDeviceID)
+ self.notifyCurrentState()
+ DispatchQueue.main.async {
+ self.onRouteChange?()
+ NotificationCenter.default.post(name: .systemAudioRouteDidChange, object: nil)
+ }
+ }
+ }
+
+ private func notifyCurrentState() {
+ let volume = getVolume()
+ let muted = getMuteState()
+ DispatchQueue.main.async {
+ self.onVolumeChange?(volume, muted)
+ NotificationCenter.default.post(name: .systemVolumeDidChange, object: nil, userInfo: ["value": volume, "muted": muted])
+ }
+ }
+
+ private func getVolume() -> Float {
+ var volume = Float32(0)
+ let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
+ if status != noErr {
+ NSLog("β οΈ Unable to fetch volume: \(status)")
+ }
+ return volume
+ }
+
+ private func getMuteState() -> Bool {
+ var mute: UInt32 = 0
+ let status = getData(selector: kAudioDevicePropertyMute, data: &mute)
+ if status != noErr {
+ // Some devices do not support mute; treat as not muted
+ return false
+ }
+ return mute != 0
+ }
+
+ private func refreshPropertyElements() {
+ volumeElement = resolveElement(selector: kAudioDevicePropertyVolumeScalar, deviceID: currentDeviceID)
+ muteElement = resolveElement(selector: kAudioDevicePropertyMute, deviceID: currentDeviceID)
+ }
+
+ private func resolveElement(selector: AudioObjectPropertySelector, deviceID: AudioDeviceID) -> AudioObjectPropertyElement? {
+ for element in candidateElements {
+ var address = makeAddress(selector: selector, element: element)
+ if propertyExists(deviceID: deviceID, address: &address) {
+ return element
+ }
+ }
+ return nil
+ }
+
+ private func preferredElements(for selector: AudioObjectPropertySelector) -> [AudioObjectPropertyElement] {
+ if let cached = cachedElement(for: selector) {
+ return [cached] + candidateElements.filter { $0 != cached }
+ }
+ return candidateElements
+ }
+
+ private func cachedElement(for selector: AudioObjectPropertySelector) -> AudioObjectPropertyElement? {
+ switch selector {
+ case kAudioDevicePropertyVolumeScalar:
+ return volumeElement
+ case kAudioDevicePropertyMute:
+ return muteElement
+ default:
+ return nil
+ }
+ }
+
+ private func cache(element: AudioObjectPropertyElement, for selector: AudioObjectPropertySelector) {
+ switch selector {
+ case kAudioDevicePropertyVolumeScalar:
+ volumeElement = element
+ case kAudioDevicePropertyMute:
+ muteElement = element
+ default:
+ break
+ }
+ }
+
+ private func makeAddress(selector: AudioObjectPropertySelector, element: AudioObjectPropertyElement) -> AudioObjectPropertyAddress {
+ AudioObjectPropertyAddress(
+ mSelector: selector,
+ mScope: kAudioDevicePropertyScopeOutput,
+ mElement: element
+ )
+ }
+
+ private func propertyExists(deviceID: AudioDeviceID, address: inout AudioObjectPropertyAddress) -> Bool {
+ withUnsafePointer(to: &address) { pointer in
+ AudioObjectHasProperty(deviceID, pointer)
+ }
+ }
+
+ private func getData(selector: AudioObjectPropertySelector, data: inout T) -> OSStatus {
+ var lastStatus: OSStatus = kAudioHardwareUnspecifiedError
+ for element in preferredElements(for: selector) {
+ var address = makeAddress(selector: selector, element: element)
+ guard propertyExists(deviceID: currentDeviceID, address: &address) else { continue }
+ var size = UInt32(MemoryLayout.size)
+ lastStatus = AudioObjectGetPropertyData(currentDeviceID, &address, 0, nil, &size, &data)
+ if lastStatus == noErr {
+ cache(element: element, for: selector)
+ return lastStatus
+ }
+ }
+ return lastStatus
+ }
+
+ private func setData(selector: AudioObjectPropertySelector, data: inout T) -> OSStatus {
+ var lastStatus: OSStatus = kAudioHardwareUnspecifiedError
+ for element in preferredElements(for: selector) {
+ var address = makeAddress(selector: selector, element: element)
+ guard propertyExists(deviceID: currentDeviceID, address: &address) else { continue }
+ let size = UInt32(MemoryLayout.size)
+ lastStatus = AudioObjectSetPropertyData(currentDeviceID, &address, 0, nil, size, &data)
+ if lastStatus == noErr {
+ cache(element: element, for: selector)
+ return lastStatus
+ }
+ }
+ return lastStatus
+ }
+}
+
+final class SystemBrightnessController {
+ static let shared = SystemBrightnessController()
+
+ var onBrightnessChange: ((Float) -> Void)?
+
+ private let notificationCenter = NotificationCenter.default
+ private var observers: [NSObjectProtocol] = []
+ private var notificationsInstalled = false
+ private var displayID: CGDirectDisplayID = CGMainDisplayID()
+
+ private init() {
+ registerExternalNotifications()
+ }
+
+ func start() {
+ notifyCurrentBrightness()
+ }
+
+ func stop() {
+ onBrightnessChange = nil
+ }
+
+ func adjust(by delta: Float) {
+ setBrightness(currentBrightness + delta)
+ }
+
+ func setBrightness(_ value: Float) {
+ let clamped = max(0, min(1, value))
+ if setBrightnessViaDisplayServices(clamped) {
+ notifyCurrentBrightness()
+ return
+ }
+ guard let service = displayService() else { return }
+ let status = IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, clamped)
+ IOObjectRelease(service)
+ if status == kIOReturnSuccess {
+ notifyCurrentBrightness()
+ } else {
+ NSLog("β οΈ Failed to set brightness via IODisplay: \(status)")
+ }
+ }
+
+ var currentBrightness: Float {
+ if let level = getBrightnessViaDisplayServices() {
+ return level
+ }
+ guard let service = displayService() else { return 0.5 }
+ var brightness: Float = 0
+ let result = IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, &brightness)
+ IOObjectRelease(service)
+ if result != kIOReturnSuccess {
+ return 0.5
+ }
+ return brightness
+ }
+
+ private func notifyCurrentBrightness() {
+ let brightness = currentBrightness
+ DispatchQueue.main.async {
+ self.onBrightnessChange?(brightness)
+ self.notificationCenter.post(name: .systemBrightnessDidChange, object: nil, userInfo: ["value": brightness])
+ }
+ }
+
+ private func displayService() -> io_service_t? {
+ var iterator = io_iterator_t()
+ guard IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IODisplayConnect"), &iterator) == KERN_SUCCESS else {
+ return nil
+ }
+ let service = IOIteratorNext(iterator)
+ IOObjectRelease(iterator)
+ return service
+ }
+
+ private func setBrightnessViaDisplayServices(_ value: Float) -> Bool {
+ let status = DisplayServicesSetBrightness(displayID, value)
+ if status == kIOReturnSuccess {
+ return true
+ }
+ // Attempt to refresh display ID in case the main display changed
+ displayID = CGMainDisplayID()
+ let retry = DisplayServicesSetBrightness(displayID, value)
+ if retry != kIOReturnSuccess {
+ NSLog("β οΈ DisplayServicesSetBrightness failed: \(retry)")
+ return false
+ }
+ return true
+ }
+
+ private func getBrightnessViaDisplayServices() -> Float? {
+ var brightness: Float = 0
+ let status = DisplayServicesGetBrightness(displayID, &brightness)
+ if status == kIOReturnSuccess {
+ return brightness
+ }
+ displayID = CGMainDisplayID()
+ let retry = DisplayServicesGetBrightness(displayID, &brightness)
+ if retry == kIOReturnSuccess {
+ return brightness
+ }
+ NSLog("β οΈ DisplayServicesGetBrightness failed: \(retry)")
+ return nil
+ }
+
+ private func registerExternalNotifications() {
+ guard !notificationsInstalled else { return }
+ let names = [
+ Notification.Name("com.apple.BezelEngine.BrightnessChanged"),
+ Notification.Name("com.apple.BezelServices.BrightnessChanged"),
+ Notification.Name("com.apple.controlcenter.display.brightness")
+ ]
+ observers = names.map { name in
+ DistributedNotificationCenter.default().addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
+ self?.notifyCurrentBrightness()
+ }
+ }
+ notificationsInstalled = true
+ }
+
+ deinit {
+ observers.forEach { DistributedNotificationCenter.default().removeObserver($0) }
+ }
+}
+
+@_silgen_name("DisplayServicesGetBrightness")
+private func DisplayServicesGetBrightness(_ display: UInt32, _ brightness: UnsafeMutablePointer) -> Int32
+
+@_silgen_name("DisplayServicesSetBrightness")
+private func DisplayServicesSetBrightness(_ display: UInt32, _ brightness: Float) -> Int32