Merge pull request #460 from Kolomaster68/fix/brightness-hud-macos-tahoe

Fix brightness HUD not working on macOS Tahoe 26.5
This commit is contained in:
Hariharan Mudaliar 2026-05-19 00:07:20 +05:30 • committed by GitHub
commit 3efc916fd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 110 additions and 2 deletions

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T>(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)
}
}

View file

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

View file

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