Der Einblendung zuvorkommen, statt sie abschalten zu wollen

Der Schluss im letzten Commit war falsch. Aus „der Dienst liegt in der
Benutzer-Domäne" hatte ich geschlossen, dass `launchctl bootout` ihn dort
aushängt. Nachgemessen, diesmal ausführend:

    Boot-out failed: 150: Operation not permitted while
    System Integrity Protection is engaged

SIP schützt den Dienst, nicht nur den Dateipfad; die Domäne spielt keine Rolle.
Ein rein lesender Spike konnte das nicht zeigen — nur der Versuch. `SystemOSD`
fliegt deshalb wieder raus: Code, der behauptet zu wirken und es nicht tut, ist
schlimmer als keiner.

Stattdessen ein `CGEventTap` auf die Lautstärketasten. Onyx regelt selbst und
verbraucht das Ereignis — was das System nie erreicht, blendet es auch nicht
ein. Preis ist die Berechtigung „Bedienungshilfen"; ohne sie bleibt alles wie
vorher, und der Dialog wird angefragt statt stumm zu scheitern.

Vorteil gegenüber dem Aushängen: es verschwindet nur die Einblendung für die
Tasten, die Onyx auch bedient. Die Feststelltaste behält ihre — beim Aushängen
wäre sie mit verschwunden, und dafür hat Onyx keinen Ersatz.

Der Rückruf steht als eigene Funktion und nicht als Verschluss in `start()`: als
Verschluss brachte er Swift 6.3.3 im Durchgang `SendNonSendable` zum Absturz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-05 23:02:35 +02:00
co-authored by Claude Opus 5
parent a3399d3a96
commit 91b83928ca
5 changed files with 228 additions and 112 deletions
+4 -4
View File
@@ -3690,13 +3690,13 @@
"de": {
"stringUnit": {
"state": "translated",
"value": "Apples Einblendung unterdrücken"
"value": "Lautstärketasten übernehmen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Suppress Apple's own overlay"
"value": "Take over the volume keys"
}
}
}
@@ -3706,13 +3706,13 @@
"de": {
"stringUnit": {
"state": "translated",
"value": "Sonst zeigt der Mac bei jedem Lautstärkeschritt zwei Anzeigen. Betrifft alle Einblendungen — auch Helligkeit und Feststelltaste. Beim Beenden von Onyx wird sie wieder eingeschaltet."
"value": "Onyx fängt die Tasten ab und regelt selbst — dann erscheint Apples eigene Einblendung nicht mehr, weil der Tastendruck das System nie erreicht. Abschalten lässt sie sich nicht: das verhindert SIP. Braucht die Berechtigung „Bedienungshilfen“."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Otherwise the Mac shows two indicators for every volume step. Affects every overlay — brightness and caps lock too. It is restored when Onyx quits."
"value": "Onyx intercepts the keys and sets the volume itself — Apple's own overlay then stays away, because the keypress never reaches the system. Switching it off is not possible: SIP prevents that. Requires the Accessibility permission."
}
}
}
+44 -9
View File
@@ -61,6 +61,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
/// Die Helligkeit des eingebauten Bildschirms. Auf Rechnern ohne lesbare
/// Helligkeit bleibt sie leer und meldet nie etwas.
private let brightness = DisplayBrightness()
/// Fängt die Lautstärketasten ab, damit Apples Einblendung nicht erscheint.
private let mediaKeys = MediaKeyTap()
/// Der Sperrbildschirm. Steht bereit, sperrt aber nur auf Verlangen.
private let lockController = LockController()
/// Der zuletzt gemeldete Stand — der erste meldet nichts, sonst begrüßte
@@ -189,10 +192,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
// Ohne diese Zeile wäre der Schalter in den Einstellungen eine
// Behauptung: gespeichert, aber ohne Wirkung.
if model.suppressSystemOSD { SystemOSD.suppress() }
model.onSuppressOSDChanged = { suppress in
suppress ? SystemOSD.suppress() : SystemOSD.restore()
}
applyMediaKeys(model.suppressSystemOSD)
model.onSuppressOSDChanged = { [weak self] on in self?.applyMediaKeys(on) }
lockController.usesFaceUnlock = model.faceUnlockEnabled
model.onFaceUnlockChanged = { [weak self] uses in
@@ -314,11 +315,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
func applicationWillTerminate(_ notification: Notification) {
// Apples Einblendung wieder einhängen. Ohne das säße jemand bis zum
// nächsten Abmelden ganz ohne Rückmeldung da — auch bei der Helligkeit,
// mit der Onyx nichts zu tun hat — und wüsste nicht, warum. Eine
// Einstellung darf nicht überleben, was sie eingestellt hat.
if model?.suppressSystemOSD == true { SystemOSD.restore() }
mediaKeys.stop()
coordinator?.stop()
menuBar?.stop()
// Der Herzschlag endet damit. Der Helfer stellt die Lüfter binnen fünf
@@ -444,6 +441,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
@objc private func lockNow() { lockController.lock() }
/// Schaltet die Übernahme der Lautstärketasten an oder aus.
///
/// **Apples Einblendung lässt sich nicht abschalten.** `launchctl bootout`
/// scheitert an SIP, egal in welcher Domäne — nachgemessen in
/// `docs/spikes/J-osd.md`. Was geht, ist ihr zuvorzukommen: das System
/// blendet nichts ein, wenn der Tastendruck es nie erreicht.
private func applyMediaKeys(_ on: Bool) {
guard on else { mediaKeys.stop(); return }
guard MediaKeyTap.isTrusted else {
// Ohne Bedienungshilfen tut der Abgriff nichts. Den Dialog anfragen
// statt stumm zu scheitern.
MediaKeyTap.requestTrust()
return
}
mediaKeys.onKey = { [weak self] key in
MainActor.assumeIsolated { self?.handleMediaKey(key) ?? false }
}
mediaKeys.start()
}
/// Wie macOS selbst: ein Sechzehntel je Anschlag.
private static let volumeStep = 1.0 / 16.0
private func handleMediaKey(_ key: MediaKeyTap.Key) -> Bool {
let current = systemAudio.outputVolume
switch key {
case .volumeUp:
systemAudio.setOutputVolume(min(1, current + Self.volumeStep))
case .volumeDown:
systemAudio.setOutputVolume(max(0, current - Self.volumeStep))
case .mute:
systemAudio.setOutputMuted(!systemAudio.outputMuted)
}
// Die Activity kommt von selbst: `observeVolume()` hört auf genau die
// Änderung, die wir hier gerade ausgelöst haben.
return true
}
/// Meldet jede Änderung der Systemlautstärke als Live Activity an.
///
/// Über `withObservationTracking` und nicht über einen eigenen Listener: