Neben ihr wären die Arme zwangsläufig gleich breit — die Silhouette zeichnet den Steg mittig um die Hardware. Ein Symbol links und ein Balken rechts hieße dann 132 Punkte Schwarz für ein Symbol. Darunter hängt die Fläche frei und wird so breit, wie ihr Inhalt es braucht. Lautstärke, Strom, Titelwechsel und Helligkeit. Gemeldet wird nur, was sich **geändert** hat: `PowerWatcher` schweigt beim ersten gelesenen Zustand, sonst begrüßte Onyx jeden Start mit „am Netz". Die Helligkeit kostete einen Spike. Von drei Wegen trägt nur `DisplayServices`, und nur für den eingebauten Bildschirm. `CoreDisplay` — der Weg, den jede Anleitung empfiehlt — gibt für alle Bildschirme 1.0 zurück, auch für den, der auf 62 % stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
4.1 KiB
Swift
102 lines
4.1 KiB
Swift
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<Float>) -> 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)")
|
|
}
|
|
}
|
|
}
|