diff --git a/DynamicIsland/helpers/CoreBrightnessDisplayClient.swift b/DynamicIsland/helpers/CoreBrightnessDisplayClient.swift new file mode 100644 index 0000000..a6e2032 --- /dev/null +++ b/DynamicIsland/helpers/CoreBrightnessDisplayClient.swift @@ -0,0 +1,82 @@ +/* + * Atoll (DynamicIsland) + * Copyright (C) 2024-2026 Atoll Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation +import ObjectiveC.runtime + +final class CoreBrightnessDisplayClient { + static let shared = CoreBrightnessDisplayClient() + + private static let builtInDisplayID: UInt64 = 0 + + private var clientInstance: NSObject? + private let getSelector = NSSelectorFromString("brightnessForDisplay:") + private let setSelector = NSSelectorFromString("setBrightness:forDisplay:") + private let notificationSelector = NSSelectorFromString("setNotificationBlock:") + + var onBrightnessChange: ((Float) -> Void)? + private var available = false + + private init() { + var loaded = false + let bundlePaths = [ + "/System/Library/PrivateFrameworks/CoreBrightness.framework", + "/System/Library/PrivateFrameworks/CoreBrightness.framework/CoreBrightness" + ] + for path in bundlePaths where !loaded { + if let bundle = Bundle(path: path) { + loaded = bundle.load() + } + } + guard loaded, let cls = NSClassFromString("DisplayBrightnessClient") as? NSObject.Type else { + NSLog("⚠️ CoreBrightnessDisplayClient: DisplayBrightnessClient class not found") + return + } + clientInstance = cls.init() + available = clientInstance != nil + } + + var isAvailable: Bool { available } + + func currentBrightness() -> Float? { + guard let clientInstance, + let getter: BrightnessGetter = methodIMP(on: clientInstance, selector: getSelector, as: BrightnessGetter.self) + else { return nil } + let value = getter(clientInstance, getSelector, Self.builtInDisplayID) + guard value >= 0, value <= 1 else { return nil } + return value + } + + func setBrightness(_ value: Float) -> Bool { + guard let clientInstance, + let setter: BrightnessSetter = methodIMP(on: clientInstance, selector: setSelector, as: BrightnessSetter.self) + else { return false } + return setter(clientInstance, setSelector, value, Self.builtInDisplayID).boolValue + } + + private typealias BrightnessGetter = @convention(c) (NSObject, Selector, UInt64) -> Float + private typealias BrightnessSetter = @convention(c) (NSObject, Selector, Float, UInt64) -> ObjCBool + + private func methodIMP(on object: NSObject, selector: Selector, as type: T.Type) -> T? { + guard let cls = object_getClass(object), + let method = class_getInstanceMethod(cls, selector) + else { return nil } + let imp = method_getImplementation(method) + return unsafeBitCast(imp, to: T.self) + } +} diff --git a/DynamicIsland/managers/SystemDisplayManager.swift b/DynamicIsland/managers/SystemDisplayManager.swift index 94195f3..93fa377 100644 --- a/DynamicIsland/managers/SystemDisplayManager.swift +++ b/DynamicIsland/managers/SystemDisplayManager.swift @@ -46,7 +46,7 @@ class SystemDisplayManager { private static func getStandardDisplayBrightness() throws -> Float { var brightness: float_t = 1 - let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect")) + let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IODisplayConnect")) defer { IOObjectRelease(service) } diff --git a/DynamicIsland/managers/SystemMediaControllers.swift b/DynamicIsland/managers/SystemMediaControllers.swift index d873e04..f61c8d7 100644 --- a/DynamicIsland/managers/SystemMediaControllers.swift +++ b/DynamicIsland/managers/SystemMediaControllers.swift @@ -476,6 +476,10 @@ final class SystemBrightnessController { private let maximumBrightnessAnimationDuration: TimeInterval = 0.3 private let brightnessAnimationDurationScale: TimeInterval = 1.6 private var lastEmittedBrightness: Float = 0.5 + private let coreBrightnessClient = CoreBrightnessDisplayClient.shared + private var pollTimer: Timer? + private let pollInterval: TimeInterval = 0.15 + private let pollChangeThreshold: Float = 0.005 private init() { registerExternalNotifications() @@ -484,12 +488,15 @@ final class SystemBrightnessController { func start() { notifyCurrentBrightness() + startPollingIfNeeded() } func stop() { onBrightnessChange = nil brightnessAnimationTimer?.invalidate() brightnessAnimationTimer = nil + pollTimer?.invalidate() + pollTimer = nil } func adjust(by delta: Float) { @@ -506,6 +513,9 @@ final class SystemBrightnessController { } var currentBrightness: Float { + if let level = coreBrightnessClient.currentBrightness() { + return level + } if let level = getBrightnessViaDisplayServices() { return level } @@ -583,6 +593,9 @@ final class SystemBrightnessController { private func applyBrightness(_ value: Float) { let clamped = max(0, min(1, value)) + if coreBrightnessClient.setBrightness(clamped) { + return + } if setBrightnessViaDisplayServices(clamped) { return } @@ -667,7 +680,8 @@ final class SystemBrightnessController { let names = [ Notification.Name("com.apple.BezelEngine.BrightnessChanged"), Notification.Name("com.apple.BezelServices.BrightnessChanged"), - Notification.Name("com.apple.controlcenter.display.brightness") + Notification.Name("com.apple.controlcenter.display.brightness"), + Notification.Name("com.apple.CoreBrightness.DisplayBrightnessChanged") ] observers = names.map { name in DistributedNotificationCenter.default().addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in @@ -677,8 +691,20 @@ final class SystemBrightnessController { notificationsInstalled = true } + private func startPollingIfNeeded() { + guard pollTimer == nil else { return } + pollTimer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] _ in + guard let self else { return } + let system = self.currentBrightness + if abs(system - self.lastEmittedBrightness) > self.pollChangeThreshold { + self.emitBrightnessChange(value: system) + } + } + } + deinit { brightnessAnimationTimer?.invalidate() + pollTimer?.invalidate() observers.forEach { DistributedNotificationCenter.default().removeObserver($0) } } }