696 lines
22 KiB
Swift
696 lines
22 KiB
Swift
import AppKit
|
|
import CoreGraphics
|
|
import Foundation
|
|
import MacToolsPluginKit
|
|
|
|
private extension NSLock {
|
|
func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
|
lock()
|
|
defer { unlock() }
|
|
return try body()
|
|
}
|
|
}
|
|
|
|
final class SystemDisplayBrightnessBackendBuilder: DisplayBrightnessBackendBuilding {
|
|
typealias Arm64ServiceResolver = ([DisplayInfo]) -> [CGDirectDisplayID: CFTypeRef]
|
|
typealias AppleBackendFactory = (DisplayInfo) -> (any DisplayBrightnessBackend)?
|
|
typealias DDCBackendFactory = (DisplayInfo, CFTypeRef?) -> (any DisplayBrightnessBackend)?
|
|
typealias SoftwareBackendFactory = (DisplayInfo) -> (any DisplayBrightnessBackend)?
|
|
|
|
private enum BackendCandidate: Equatable {
|
|
case appleNative
|
|
case ddc
|
|
case gamma
|
|
case shade
|
|
|
|
init(kind: DisplayBrightnessBackendKind) {
|
|
switch kind {
|
|
case .appleNative:
|
|
self = .appleNative
|
|
case .ddc:
|
|
self = .ddc
|
|
case .gamma:
|
|
self = .gamma
|
|
case .shade:
|
|
self = .shade
|
|
}
|
|
}
|
|
}
|
|
|
|
private let resolveArm64Services: Arm64ServiceResolver
|
|
private let appleFactory: AppleBackendFactory
|
|
private let ddcFactory: DDCBackendFactory
|
|
private let gammaFactory: SoftwareBackendFactory
|
|
private let shadeFactory: SoftwareBackendFactory
|
|
|
|
init(
|
|
resolveArm64Services: @escaping Arm64ServiceResolver = Arm64DDCServiceMatcher.resolveServices,
|
|
appleFactory: AppleBackendFactory? = nil,
|
|
ddcFactory: DDCBackendFactory? = nil,
|
|
gammaFactory: SoftwareBackendFactory? = nil,
|
|
shadeFactory: SoftwareBackendFactory? = nil
|
|
) {
|
|
let displayServicesBridge = DisplayServicesBrightnessBridge()
|
|
|
|
self.resolveArm64Services = resolveArm64Services
|
|
self.appleFactory = appleFactory ?? { display in
|
|
let backend = AppleNativeBrightnessBackend(
|
|
display: display,
|
|
bridge: displayServicesBridge
|
|
)
|
|
|
|
do {
|
|
_ = try backend.readBrightness()
|
|
return backend
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
self.ddcFactory = ddcFactory ?? { display, matchedService in
|
|
if let transport = Arm64DDCTransport(display: display, service: nil) {
|
|
return DDCBrightnessBackend(display: display, transport: transport)
|
|
}
|
|
|
|
if let matchedService,
|
|
let transport = Arm64DDCTransport(display: display, service: matchedService) {
|
|
return DDCBrightnessBackend(display: display, transport: transport)
|
|
}
|
|
|
|
if let transport = IntelDDCTransport(display: display) {
|
|
return DDCBrightnessBackend(display: display, transport: transport)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
self.gammaFactory = gammaFactory ?? { display in
|
|
GammaBrightnessBackend(display: display)
|
|
}
|
|
self.shadeFactory = shadeFactory ?? { display in
|
|
ShadeBrightnessBackend(display: display)
|
|
}
|
|
}
|
|
|
|
func backends(
|
|
for displays: [DisplayInfo],
|
|
previous: [CGDirectDisplayID: any DisplayBrightnessBackend]
|
|
) -> [CGDirectDisplayID: any DisplayBrightnessBackend] {
|
|
var arm64Services: [CGDirectDisplayID: CFTypeRef]?
|
|
var result: [CGDirectDisplayID: any DisplayBrightnessBackend] = [:]
|
|
|
|
for display in displays {
|
|
for candidate in backendCandidates(for: display) {
|
|
guard let backend = makeBackend(
|
|
candidate,
|
|
for: display,
|
|
in: displays,
|
|
previous: previous,
|
|
cache: &arm64Services
|
|
) else {
|
|
continue
|
|
}
|
|
|
|
result[display.id] = backend
|
|
break
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func fallbackBackend(
|
|
after failedBackend: any DisplayBrightnessBackend,
|
|
for display: DisplayInfo,
|
|
previous: [CGDirectDisplayID: any DisplayBrightnessBackend]
|
|
) -> (any DisplayBrightnessBackend)? {
|
|
let candidates = backendCandidates(for: display)
|
|
guard let failedIndex = candidates.firstIndex(of: BackendCandidate(kind: failedBackend.kind)) else {
|
|
return nil
|
|
}
|
|
|
|
var arm64Services: [CGDirectDisplayID: CFTypeRef]?
|
|
for candidate in candidates.dropFirst(failedIndex + 1) {
|
|
guard let backend = makeBackend(
|
|
candidate,
|
|
for: display,
|
|
in: [display],
|
|
previous: previous.filter { $0.key != display.id },
|
|
cache: &arm64Services
|
|
) else {
|
|
continue
|
|
}
|
|
|
|
return backend
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func backendCandidates(for display: DisplayInfo) -> [BackendCandidate] {
|
|
if display.isBuiltin {
|
|
return [.appleNative, .gamma, .shade]
|
|
}
|
|
|
|
if display.supportsAppleNativeBrightness {
|
|
return [.appleNative, .ddc, .gamma, .shade]
|
|
}
|
|
|
|
return [.ddc, .gamma, .shade]
|
|
}
|
|
|
|
private func makeBackend(
|
|
_ candidate: BackendCandidate,
|
|
for display: DisplayInfo,
|
|
in displays: [DisplayInfo],
|
|
previous: [CGDirectDisplayID: any DisplayBrightnessBackend],
|
|
cache arm64Services: inout [CGDirectDisplayID: CFTypeRef]?
|
|
) -> (any DisplayBrightnessBackend)? {
|
|
switch candidate {
|
|
case .appleNative:
|
|
return appleBackend(for: display, previous: previous)
|
|
case .ddc:
|
|
return ddcBackend(
|
|
for: display,
|
|
in: displays,
|
|
previous: previous,
|
|
cache: &arm64Services
|
|
)
|
|
case .gamma:
|
|
return softwareBackend(
|
|
kind: .gamma,
|
|
label: "Gamma",
|
|
for: display,
|
|
previous: previous,
|
|
factory: gammaFactory
|
|
)
|
|
case .shade:
|
|
return softwareBackend(
|
|
kind: .shade,
|
|
label: "Shade",
|
|
for: display,
|
|
previous: previous,
|
|
factory: shadeFactory
|
|
)
|
|
}
|
|
}
|
|
|
|
private func appleBackend(
|
|
for display: DisplayInfo,
|
|
previous: [CGDirectDisplayID: any DisplayBrightnessBackend]
|
|
) -> (any DisplayBrightnessBackend)? {
|
|
guard display.isAppleDisplay else {
|
|
return nil
|
|
}
|
|
|
|
guard let backend = reuse(previous[display.id], kind: .appleNative, display: display)
|
|
?? appleFactory(display) else {
|
|
return nil
|
|
}
|
|
|
|
DisplayBrightnessLog.backend.debug(
|
|
"selected Apple native brightness backend for \(display.name, privacy: .public)"
|
|
)
|
|
return backend
|
|
}
|
|
|
|
private func softwareBackend(
|
|
kind: DisplayBrightnessBackendKind,
|
|
label: String,
|
|
for display: DisplayInfo,
|
|
previous: [CGDirectDisplayID: any DisplayBrightnessBackend],
|
|
factory: SoftwareBackendFactory
|
|
) -> (any DisplayBrightnessBackend)? {
|
|
guard let backend = reuse(previous[display.id], kind: kind, display: display)
|
|
?? factory(display) else {
|
|
return nil
|
|
}
|
|
|
|
DisplayBrightnessLog.backend.info(
|
|
"using \(label, privacy: .public) software brightness fallback for \(display.name, privacy: .public)"
|
|
)
|
|
return backend
|
|
}
|
|
|
|
private func ddcBackend(
|
|
for display: DisplayInfo,
|
|
in displays: [DisplayInfo],
|
|
previous: [CGDirectDisplayID: any DisplayBrightnessBackend],
|
|
cache arm64Services: inout [CGDirectDisplayID: CFTypeRef]?
|
|
) -> (any DisplayBrightnessBackend)? {
|
|
guard let backend = reuse(previous[display.id], kind: .ddc, display: display)
|
|
?? ddcFactory(display, resolvedArm64Service(for: display, in: displays, cache: &arm64Services)) else {
|
|
return nil
|
|
}
|
|
|
|
DisplayBrightnessLog.backend.debug(
|
|
"selected DDC brightness backend for \(display.name, privacy: .public)"
|
|
)
|
|
return backend
|
|
}
|
|
|
|
private func resolvedArm64Service(
|
|
for display: DisplayInfo,
|
|
in displays: [DisplayInfo],
|
|
cache arm64Services: inout [CGDirectDisplayID: CFTypeRef]?
|
|
) -> CFTypeRef? {
|
|
if arm64Services == nil {
|
|
arm64Services = resolveArm64Services(displays)
|
|
}
|
|
|
|
return arm64Services?[display.id]
|
|
}
|
|
|
|
private func reuse(
|
|
_ previous: (any DisplayBrightnessBackend)?,
|
|
kind: DisplayBrightnessBackendKind,
|
|
display: DisplayInfo
|
|
) -> (any DisplayBrightnessBackend)? {
|
|
guard let backend = previous, backend.kind == kind else {
|
|
return nil
|
|
}
|
|
|
|
backend.display = display
|
|
return backend
|
|
}
|
|
}
|
|
|
|
final class AppleNativeBrightnessBackend: DisplayBrightnessBackend, @unchecked Sendable {
|
|
let kind: DisplayBrightnessBackendKind = .appleNative
|
|
|
|
private let lock = NSLock()
|
|
private var _display: DisplayInfo
|
|
var display: DisplayInfo {
|
|
get { lock.withLock { _display } }
|
|
set { lock.withLock { _display = newValue } }
|
|
}
|
|
|
|
private let bridge: DisplayServicesBrightnessBridge
|
|
|
|
init(
|
|
display: DisplayInfo,
|
|
bridge: DisplayServicesBrightnessBridge
|
|
) {
|
|
self._display = display
|
|
self.bridge = bridge
|
|
}
|
|
|
|
func readBrightness() throws -> Double {
|
|
let displayID = lock.withLock { _display.id }
|
|
return try bridge.readBrightness(displayID: displayID)
|
|
}
|
|
|
|
func writeBrightness(_ value: Double) throws {
|
|
let displayID = lock.withLock { _display.id }
|
|
try bridge.writeBrightness(value, displayID: displayID)
|
|
}
|
|
|
|
func cleanup() {}
|
|
}
|
|
|
|
final class DDCBrightnessBackend: DisplayBrightnessBackend, @unchecked Sendable {
|
|
let kind: DisplayBrightnessBackendKind = .ddc
|
|
|
|
private let lock = NSLock()
|
|
private var _display: DisplayInfo
|
|
var display: DisplayInfo {
|
|
get { lock.withLock { _display } }
|
|
set { lock.withLock { _display = newValue } }
|
|
}
|
|
|
|
private let transport: any DDCBrightnessTransport
|
|
private var maximumValue: UInt16
|
|
|
|
init?(display: DisplayInfo, transport: any DDCBrightnessTransport) {
|
|
guard !display.isBuiltin else {
|
|
return nil
|
|
}
|
|
|
|
self._display = display
|
|
self.transport = transport
|
|
self.maximumValue = (try? transport.readBrightness().maximum).flatMap(Self.validMaximum) ?? 100
|
|
}
|
|
|
|
func readBrightness() throws -> Double {
|
|
let brightness = try transport.readBrightness()
|
|
return lock.withLock {
|
|
maximumValue = Self.validMaximum(brightness.maximum) ?? maximumValue
|
|
return maximumValue == 0 ? 1 : Double(brightness.current) / Double(maximumValue)
|
|
}
|
|
}
|
|
|
|
func writeBrightness(_ value: Double) throws {
|
|
let clampedValue = max(0, min(value, 1))
|
|
let rawValue = lock.withLock {
|
|
UInt16((Double(maximumValue) * clampedValue).rounded())
|
|
}
|
|
try transport.writeBrightness(rawValue)
|
|
}
|
|
|
|
func cleanup() {}
|
|
|
|
private static func validMaximum(_ maximum: UInt16) -> UInt16? {
|
|
maximum == 0 ? nil : maximum
|
|
}
|
|
}
|
|
|
|
final class GammaBrightnessBackend: DisplayBrightnessBackend, @unchecked Sendable {
|
|
private struct TransferTable {
|
|
let red: [CGGammaValue]
|
|
let green: [CGGammaValue]
|
|
let blue: [CGGammaValue]
|
|
}
|
|
|
|
let kind: DisplayBrightnessBackendKind = .gamma
|
|
|
|
private let lock = NSLock()
|
|
private var _display: DisplayInfo
|
|
var display: DisplayInfo {
|
|
get { lock.withLock { _display } }
|
|
set { lock.withLock { _display = newValue } }
|
|
}
|
|
|
|
private var originalTransferTable: TransferTable?
|
|
private var currentBrightness = 1.0
|
|
|
|
init?(display: DisplayInfo) {
|
|
guard Self.canControl(displayID: display.id) else {
|
|
return nil
|
|
}
|
|
|
|
self._display = display
|
|
}
|
|
|
|
func readBrightness() throws -> Double {
|
|
lock.withLock { currentBrightness }
|
|
}
|
|
|
|
func writeBrightness(_ value: Double) throws {
|
|
let clampedValue = max(0, min(value, 1))
|
|
let (displayID, transferTable) = try loadOriginalTransferTableIfNeeded()
|
|
let red = transferTable.red.map { $0 * Float(clampedValue) }
|
|
let green = transferTable.green.map { $0 * Float(clampedValue) }
|
|
let blue = transferTable.blue.map { $0 * Float(clampedValue) }
|
|
|
|
let result = CGSetDisplayTransferByTable(
|
|
displayID,
|
|
UInt32(red.count),
|
|
red,
|
|
green,
|
|
blue
|
|
)
|
|
|
|
guard result == .success else {
|
|
throw DisplayBrightnessControllerError.softwareBrightnessFailed
|
|
}
|
|
|
|
lock.withLock { currentBrightness = clampedValue }
|
|
}
|
|
|
|
func cleanup() {
|
|
let snapshot: (id: CGDirectDisplayID, table: TransferTable)? = lock.withLock {
|
|
guard let originalTransferTable else { return nil }
|
|
return (_display.id, originalTransferTable)
|
|
}
|
|
|
|
guard let snapshot else { return }
|
|
|
|
_ = CGSetDisplayTransferByTable(
|
|
snapshot.id,
|
|
UInt32(snapshot.table.red.count),
|
|
snapshot.table.red,
|
|
snapshot.table.green,
|
|
snapshot.table.blue
|
|
)
|
|
lock.withLock { currentBrightness = 1 }
|
|
}
|
|
|
|
private func loadOriginalTransferTableIfNeeded() throws -> (CGDirectDisplayID, TransferTable) {
|
|
// The first-time capture must be atomic w.r.t. concurrent writeBrightness()
|
|
// calls: if the read of the live LUT happened outside the lock, a racing
|
|
// writer could have already dimmed the display, and we'd cache that dimmed
|
|
// LUT as the "original" — cleanup() would then restore the wrong curve and
|
|
// never return the display to full brightness. Capture under the lock; it is
|
|
// a one-time cost (subsequent calls hit the cache and return immediately).
|
|
return try lock.withLock {
|
|
let displayID = _display.id
|
|
let displayName = _display.name
|
|
|
|
if let cachedTable = originalTransferTable {
|
|
return (displayID, cachedTable)
|
|
}
|
|
|
|
let transferTable = try Self.readTransferTable(displayID: displayID, displayName: displayName)
|
|
originalTransferTable = transferTable
|
|
return (displayID, transferTable)
|
|
}
|
|
}
|
|
|
|
private static func readTransferTable(
|
|
displayID: CGDirectDisplayID,
|
|
displayName: String
|
|
) throws -> TransferTable {
|
|
var sampleCount: UInt32 = 0
|
|
let countResult = CGGetDisplayTransferByTable(displayID, 0, nil, nil, nil, &sampleCount)
|
|
|
|
guard countResult == .success, sampleCount > 0 else {
|
|
throw DisplayBrightnessControllerError.brightnessUnavailable(displayName: displayName)
|
|
}
|
|
|
|
let red = UnsafeMutablePointer<CGGammaValue>.allocate(capacity: Int(sampleCount))
|
|
let green = UnsafeMutablePointer<CGGammaValue>.allocate(capacity: Int(sampleCount))
|
|
let blue = UnsafeMutablePointer<CGGammaValue>.allocate(capacity: Int(sampleCount))
|
|
defer {
|
|
red.deallocate()
|
|
green.deallocate()
|
|
blue.deallocate()
|
|
}
|
|
|
|
let readResult = CGGetDisplayTransferByTable(
|
|
displayID,
|
|
sampleCount,
|
|
red,
|
|
green,
|
|
blue,
|
|
&sampleCount
|
|
)
|
|
|
|
guard readResult == .success else {
|
|
throw DisplayBrightnessControllerError.brightnessUnavailable(displayName: displayName)
|
|
}
|
|
|
|
return TransferTable(
|
|
red: Array(UnsafeBufferPointer(start: red, count: Int(sampleCount))),
|
|
green: Array(UnsafeBufferPointer(start: green, count: Int(sampleCount))),
|
|
blue: Array(UnsafeBufferPointer(start: blue, count: Int(sampleCount)))
|
|
)
|
|
}
|
|
|
|
private static func canControl(displayID: CGDirectDisplayID) -> Bool {
|
|
var sampleCount: UInt32 = 0
|
|
return CGGetDisplayTransferByTable(displayID, 0, nil, nil, nil, &sampleCount) == .success
|
|
&& sampleCount > 0
|
|
}
|
|
}
|
|
|
|
final class ShadeBrightnessBackend: DisplayBrightnessBackend, @unchecked Sendable {
|
|
let kind: DisplayBrightnessBackendKind = .shade
|
|
|
|
private let lock = NSLock()
|
|
private var _display: DisplayInfo
|
|
var display: DisplayInfo {
|
|
get { lock.withLock { _display } }
|
|
set { lock.withLock { _display = newValue } }
|
|
}
|
|
|
|
private let overlayController = ShadeOverlayController()
|
|
private var currentBrightness = 1.0
|
|
|
|
init?(display: DisplayInfo) {
|
|
guard Self.screen(for: display.id) != nil else {
|
|
return nil
|
|
}
|
|
|
|
self._display = display
|
|
}
|
|
|
|
func readBrightness() throws -> Double {
|
|
refreshOverlayFrameIfNeeded()
|
|
return lock.withLock { currentBrightness }
|
|
}
|
|
|
|
func writeBrightness(_ value: Double) throws {
|
|
let clampedValue = max(0, min(value, 1))
|
|
let displayID = lock.withLock { _display.id }
|
|
guard Self.screen(for: displayID) != nil else {
|
|
throw DisplayBrightnessControllerError.displayUnavailable(displayID: displayID)
|
|
}
|
|
|
|
applyOverlay(brightness: clampedValue)
|
|
lock.withLock { currentBrightness = clampedValue }
|
|
}
|
|
|
|
func cleanup() {
|
|
hideOverlay()
|
|
lock.withLock { currentBrightness = 1 }
|
|
}
|
|
|
|
private func refreshOverlayFrameIfNeeded() {
|
|
let (displayID, brightness): (CGDirectDisplayID, Double) = lock.withLock {
|
|
(_display.id, currentBrightness)
|
|
}
|
|
guard brightness < 0.999, Self.screen(for: displayID) != nil else {
|
|
return
|
|
}
|
|
|
|
applyOverlay(brightness: brightness)
|
|
}
|
|
|
|
private func applyOverlay(brightness: Double) {
|
|
let displayID = display.id
|
|
let overlayController = self.overlayController
|
|
|
|
if Thread.isMainThread {
|
|
MainActor.assumeIsolated {
|
|
guard let screen = Self.screen(for: displayID) else {
|
|
return
|
|
}
|
|
|
|
overlayController.apply(brightness: brightness, on: screen)
|
|
}
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.sync {
|
|
MainActor.assumeIsolated {
|
|
guard let screen = Self.screen(for: displayID) else {
|
|
return
|
|
}
|
|
|
|
overlayController.apply(brightness: brightness, on: screen)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func hideOverlay() {
|
|
let overlayController = self.overlayController
|
|
|
|
if Thread.isMainThread {
|
|
MainActor.assumeIsolated {
|
|
overlayController.hide()
|
|
}
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.sync {
|
|
MainActor.assumeIsolated {
|
|
overlayController.hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func screen(for displayID: CGDirectDisplayID) -> NSScreen? {
|
|
NSScreen.screens.first(where: {
|
|
($0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value == displayID
|
|
})
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ShadeOverlayController {
|
|
private var window: NSWindow?
|
|
|
|
func apply(brightness: Double, on screen: NSScreen) {
|
|
let alpha = max(0, min(1 - brightness, 0.92))
|
|
|
|
guard alpha > 0.001 else {
|
|
hide()
|
|
return
|
|
}
|
|
|
|
let window = self.window ?? self.makeWindow(screen: screen)
|
|
window.setFrame(screen.frame, display: true)
|
|
window.alphaValue = alpha
|
|
window.orderFrontRegardless()
|
|
self.window = window
|
|
}
|
|
|
|
func hide() {
|
|
guard let window else {
|
|
return
|
|
}
|
|
|
|
window.orderOut(nil)
|
|
self.window = nil
|
|
}
|
|
|
|
private func makeWindow(screen: NSScreen) -> NSWindow {
|
|
let window = NSWindow(
|
|
contentRect: screen.frame,
|
|
styleMask: .borderless,
|
|
backing: .buffered,
|
|
defer: false,
|
|
screen: screen
|
|
)
|
|
window.level = .screenSaver
|
|
window.backgroundColor = .black
|
|
window.isOpaque = false
|
|
window.ignoresMouseEvents = true
|
|
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
|
|
window.hasShadow = false
|
|
return window
|
|
}
|
|
}
|
|
|
|
final class DisplayServicesBrightnessBridge: @unchecked Sendable {
|
|
private typealias GetBrightnessFunction = @convention(c) (
|
|
CGDirectDisplayID,
|
|
UnsafeMutablePointer<Float>
|
|
) -> Int32
|
|
private typealias SetBrightnessFunction = @convention(c) (CGDirectDisplayID, Float) -> Int32
|
|
|
|
private let getBrightness: GetBrightnessFunction?
|
|
private let setBrightness: SetBrightnessFunction?
|
|
|
|
init() {
|
|
let handle = dlopen(
|
|
"/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices",
|
|
RTLD_LAZY
|
|
)
|
|
self.getBrightness = Self.loadSymbol("DisplayServicesGetBrightness", from: handle)
|
|
self.setBrightness = Self.loadSymbol("DisplayServicesSetBrightness", from: handle)
|
|
}
|
|
|
|
func readBrightness(displayID: CGDirectDisplayID) throws -> Double {
|
|
guard let getBrightness else {
|
|
throw DisplayBrightnessControllerError.nativeAPINotAvailable
|
|
}
|
|
|
|
var value: Float = 0
|
|
guard getBrightness(displayID, &value) == 0 else {
|
|
throw DisplayBrightnessControllerError.genericBrightnessUnavailable
|
|
}
|
|
|
|
return Double(max(0, min(value, 1)))
|
|
}
|
|
|
|
func writeBrightness(_ value: Double, displayID: CGDirectDisplayID) throws {
|
|
guard let setBrightness else {
|
|
throw DisplayBrightnessControllerError.nativeAPINotAvailable
|
|
}
|
|
|
|
guard setBrightness(displayID, Float(max(0, min(value, 1)))) == 0 else {
|
|
throw DisplayBrightnessControllerError.nativeBrightnessWriteFailed
|
|
}
|
|
}
|
|
|
|
private static func loadSymbol<T>(
|
|
_ symbol: String,
|
|
from handle: UnsafeMutableRawPointer?
|
|
) -> T? {
|
|
guard let symbolPointer = dlsym(handle, symbol) else {
|
|
return nil
|
|
}
|
|
|
|
return unsafeBitCast(symbolPointer, to: T.self)
|
|
}
|
|
}
|