import AppKit import Observation import os private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Brightness") /// Die Helligkeit des eingebauten Bildschirms — gelesen, nicht gesetzt. /// /// **Nur der eingebaute.** Ein Spike über drei Wege hat ergeben: von /// `DisplayServices` bekommt man für externe Monitore Fehler 1000, `CoreDisplay` /// liefert für jeden Bildschirm stur 1,0 und ist damit wertlos, und /// `IODisplayConnect` gibt es auf Apple Silicon gar nicht mehr. Siehe /// `Spikes/brightness-probe.swift`. /// /// **Und nicht abgetastet, sondern gemeldet.** Eine Notch, die zehnmal je /// Sekunde nach der Helligkeit fragt, hält den Rechner wach — den ganzen Tag, /// für ein Ereignis alle paar Stunden. `DisplayServices` kann benachrichtigen; /// gäbe es das nicht, wäre diese Anzeige den Preis nicht wert gewesen. /// /// **Private API, und deshalb mit Netz.** Fehlt eines der Symbole — bei einem /// Systemwechsel jederzeit möglich —, bleibt `level` auf `nil` und Onyx zeigt /// schlicht keine Helligkeit an. Nichts anzuzeigen ist ein hinnehmbarer /// Ausfall; abzustürzen wäre keiner. @MainActor @Observable public final class DisplayBrightness { /// Der zuletzt gemeldete Stand, 0 bis 1. `nil` heißt: nicht lesbar. public private(set) var level: Double? /// Ob dieser Rechner überhaupt eine lesbare Helligkeit hat. public var isAvailable: Bool { display != nil } @ObservationIgnored private let display: CGDirectDisplayID? @ObservationIgnored private let read: GetBrightness? private typealias GetBrightness = @convention(c) (CGDirectDisplayID, UnsafeMutablePointer) -> Int32 private typealias Callback = @convention(c) (UnsafeRawPointer?, CGDirectDisplayID, CFString?, Double) -> Void private typealias Register = @convention(c) (CGDirectDisplayID, UnsafeRawPointer?, Callback) -> Int32 /// Der Rückruf ist ein reiner C-Zeiger und kann nichts einfangen. Die eine /// laufende Instanz steht deshalb hier — schwach, damit sie sich beenden /// lässt. @ObservationIgnored private nonisolated(unsafe) static weak var current: DisplayBrightness? private static let frameworkPath = "/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices" public init() { display = NSScreen.screens .compactMap { $0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID } .first { CGDisplayIsBuiltin($0) != 0 } guard let handle = dlopen(Self.frameworkPath, RTLD_LAZY) else { log.notice("DisplayServices nicht ladbar — keine Helligkeitsanzeige") read = nil return } read = dlsym(handle, "DisplayServicesGetBrightness") .map { unsafeBitCast($0, to: GetBrightness.self) } Self.current = self refresh() subscribe(handle) } /// Liest den aktuellen Stand einmal. public func refresh() { guard let display, let read else { level = nil; return } var value: Float = 0 // Status ungleich null heißt: dieser Bildschirm hat keine Helligkeit, // die man lesen könnte. Kein Fehler, nur keine Antwort. guard read(display, &value) == 0 else { level = nil; return } level = Double(min(max(value, 0), 1)) } private func subscribe(_ handle: UnsafeMutableRawPointer) { guard let display, let symbol = dlsym(handle, "DisplayServicesRegisterForBrightnessChangeNotifications") else { log.notice("Keine Helligkeitsmeldungen — Anzeige bleibt beim Startwert") return } let callback: Callback = { _, _, _, value in // Der Rückruf kommt aus einem fremden Faden. Task { @MainActor in DisplayBrightness.current?.level = min(max(value, 0), 1) } } let status = unsafeBitCast(symbol, to: Register.self)(display, nil, callback) if status != 0 { log.notice("Anmeldung für Helligkeitsmeldungen fehlgeschlagen: \(status, privacy: .public)") } } }