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:
co-authored by
Claude Opus 5
parent
a3399d3a96
commit
91b83928ca
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user