Merge pull request #48 from Ebullioscopic/feature/hud

Feature/hud
This commit is contained in:
Hariharan Mudaliar 2025-10-18 21:39:00 +05:30 • committed by GitHub
commit 0f602b04e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 775 additions and 576 deletions

1
.gitignore vendored
View file

@ -88,6 +88,7 @@ ONBOARDING_ENHANCEMENT.md
SHEET_PRESENTATION_FIX.md
lock_mode_reference.md
liquid_glass.md
huds.md
MoveToSky.xcodeproj
/MoveToSky/

View file

@ -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 = (

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54406E62CD4987A00C7AFD9"
BuildableName = "DynamicIsland.app"
BlueprintName = "DynamicIsland"
ReferencedContainer = "container:DynamicIsland.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<PathRunnable
runnableDebuggingMode = "0"
BundleIdentifier = "com.karthikinformationtechnology.DynamicIsland"
FilePath = "/Applications/DynamicIsland.app">
</PathRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54406E62CD4987A00C7AFD9"
BuildableName = "DynamicIsland.app"
BlueprintName = "DynamicIsland"
ReferencedContainer = "container:DynamicIsland.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54406E62CD4987A00C7AFD9"
BuildableName = "DynamicIsland.app"
BlueprintName = "DynamicIsland"
ReferencedContainer = "container:DynamicIsland.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -93,10 +93,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var cancellables = Set<AnyCancellable>()
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()

View file

@ -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<MediaKeyInterceptor>.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<CGEvent>? {
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

View file

@ -1,346 +1,96 @@
//
// 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?
private let volumeController = SystemVolumeController.shared
private let brightnessController = SystemBrightnessController.shared
private let mediaKeyInterceptor = MediaKeyInterceptor.shared
// Adaptive polling timer
private var pollingTimer: Timer?
private var currentPollingInterval: TimeInterval = 2.0
private let volumeStep: Float = 1.0 / 16.0
private let brightnessStep: Float = 1.0 / 16.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 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 }
func startObserving(volumeEnabled: Bool, brightnessEnabled: Bool) {
self.volumeEnabled = volumeEnabled
self.brightnessEnabled = brightnessEnabled
// 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)
)
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
volumeController.onVolumeChange = { [weak self] volume, muted in
guard let self, self.volumeEnabled else { return }
let value = muted ? 0 : volume
self.sendVolumeNotification(value: value)
}
// 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
volumeController.onRouteChange = { [weak self] in
guard let self, self.volumeEnabled else { return }
self.sendVolumeNotification(value: self.volumeController.isMuted ? 0 : self.volumeController.currentVolume)
}
volumeController.start()
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()
}
brightnessController.onBrightnessChange = { [weak self] brightness in
guard let self, self.brightnessEnabled else { return }
self.sendBrightnessNotification(value: brightness)
}
brightnessController.start()
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()
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
)
}
@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
func update(volumeEnabled: Bool, brightnessEnabled: Bool) {
self.volumeEnabled = volumeEnabled
self.brightnessEnabled = brightnessEnabled
mediaKeyInterceptor.configuration = MediaKeyConfiguration(
interceptVolume: volumeEnabled,
interceptBrightness: brightnessEnabled
)
var deviceID: AudioDeviceID = 0
var propertySize = UInt32(MemoryLayout<AudioDeviceID>.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<UInt32>.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 stopObserving() {
mediaKeyInterceptor.stop()
mediaKeyInterceptor.delegate = nil
func handleVolumeKeyEvent() {
// Mark as user-initiated and trigger burst mode
lastVolumeChangeTime = Date()
volumeController.stop()
volumeController.onVolumeChange = nil
volumeController.onRouteChange = nil
// Switch to burst mode immediately
startPollingTimer(interval: rapidPollingInterval)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.handleVolumeKeyPress()
}
brightnessController.stop()
brightnessController.onBrightnessChange = nil
}
func handleBrightnessKeyEvent() {
// Mark as user-initiated and trigger burst mode
lastBrightnessChangeTime = Date()
// MARK: - MediaKeyInterceptorDelegate
// Switch to burst mode immediately
startPollingTimer(interval: rapidPollingInterval)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.handleBrightnessKeyPress()
}
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, isRepeat: Bool) {
guard volumeEnabled else { return }
let delta = (direction == .up ? volumeStep : -volumeStep)
volumeController.adjust(by: delta)
}
// 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
func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor) {
guard volumeEnabled else { return }
volumeController.toggleMute()
}
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
}
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, isRepeat: Bool) {
guard brightnessEnabled else { return }
let delta = (direction == .up ? brightnessStep : -brightnessStep)
brightnessController.adjust(by: delta)
}
// 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
}
}
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
}
// MARK: - Internal Notification Methods
// MARK: - HUD Dispatch
private func sendVolumeNotification(value: Float) {
coordinator?.toggleSneakPeek(
@ -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<AudioObjectPropertyAddress>,
_ inClientData: UnsafeMutableRawPointer?
) -> OSStatus {
guard let inClientData = inClientData else { return noErr }
let observer = Unmanaged<SystemChangesObserver>.fromOpaque(inClientData).takeUnretainedValue()
// Handle on main thread
DispatchQueue.main.async {
observer.audioRouteChanged()
}
return noErr
}

View file

@ -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()
}
}

View file

@ -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<T>(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<T>.size)
lastStatus = AudioObjectGetPropertyData(currentDeviceID, &address, 0, nil, &size, &data)
if lastStatus == noErr {
cache(element: element, for: selector)
return lastStatus
}
}
return lastStatus
}
private func setData<T>(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<T>.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<Float>) -> Int32
@_silgen_name("DisplayServicesSetBrightness")
private func DisplayServicesSetBrightness(_ display: UInt32, _ brightness: Float) -> Int32