Files
onyx/Packages/OnyxKit/Sources/MetricsProvider/DisplayBrightness.swift
T
ScarriffleandClaude Opus 5 9b600de9a9 Live Activities unter der Notch, nicht daneben
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>
2026-09-05 21:05:13 +02:00

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