Zwei Anzeigen für einen Handgriff sind eine zu viel

Apples eigene Einblendung lässt sich aushängen, ohne Root und ohne SIP
anzufassen: `com.apple.OSDUIHelper` liegt in der GUI-Domäne des angemeldeten
Benutzers, nicht im System. `launchctl bootout` dort hinein genügt.

Standardmäßig aus, denn es verschwindet **jede** Einblendung — auch Helligkeit
und Feststelltaste, und für die Feststelltaste hat Onyx keinen Ersatz. Beim
Beenden wird wieder eingehängt: eine Einstellung darf nicht überleben, was sie
eingestellt hat.

Dazu zwei Fehler im Anlernen behoben. Der Posenname stand als roher Schlüssel da
— die Zeichenketten liegen im Bündel von `FaceProvider`, gesucht wurden sie im
Hauptbündel. Und „Gesicht vergleichen" blieb nach dem Anlernen grau, weil
`isEnrolled` bei jedem Aufruf den Schlüsselbund las: richtig, aber für SwiftUI
unsichtbar, weil es nichts gab, woran eine Ansicht die Änderung hätte bemerken
können.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-05 22:26:41 +02:00
co-authored by Claude Opus 5
parent fd29df51cf
commit a3399d3a96
9 changed files with 221 additions and 8 deletions
@@ -16,11 +16,14 @@ public final class FaceEnrollment {
public enum Pose: String, CaseIterable, Sendable {
case straight, left, right
public var localizedName: LocalizedStringKey {
/// Aufgelöst **hier** und nicht als Schlüssel weitergereicht: die
/// Zeichenketten liegen im Bündel dieses Moduls, und wer sie anderswo
/// nachschlägt, bekommt den Schlüssel selbst zu sehen.
public var localizedName: String {
switch self {
case .straight: "face.pose.straight"
case .left: "face.pose.left"
case .right: "face.pose.right"
case .straight: String(localized: "face.pose.straight", bundle: .module)
case .left: String(localized: "face.pose.left", bundle: .module)
case .right: String(localized: "face.pose.right", bundle: .module)
}
}
}
@@ -41,10 +41,24 @@ public final class LockController {
/// Ob Gesichtserkennung überhaupt benutzt werden soll.
public var usesFaceUnlock = true
public init() {}
public init() {
refreshEnrollment()
}
/// Ob ein Gesicht angelernt ist.
public var isEnrolled: Bool { !store.load().isEmpty }
///
/// **Gespeichert und nicht berechnet.** Als berechnete Eigenschaft las sie
/// bei jedem Aufruf den Schlüsselbund — richtig, aber für SwiftUI
/// unsichtbar: es gab nichts, woran eine Ansicht hätte merken können, dass
/// gerade ein Gesicht angelernt wurde. Der Schalter „Gesicht vergleichen"
/// blieb deshalb grau, bis man das Fenster neu öffnete.
public private(set) var isEnrolled = false
/// Liest nach, ob ein Gesicht hinterlegt ist. Nach dem Anlernen und nach
/// dem Vergessen zu rufen.
public func refreshEnrollment() {
isEnrolled = !store.load().isEmpty
}
// MARK: - Sperren
@@ -0,0 +1,78 @@
import Foundation
import os
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "OSD")
/// Apples eigene Einblendung für Lautstärke, Helligkeit und Feststelltaste.
///
/// Solange sie erscheint, zeigt der Mac bei jedem Lautstärkeschritt **zwei**
/// Anzeigen: Apples Kasten in der Bildschirmmitte und die Live Activity unter
/// der Notch. Eine davon ist eine zu viel.
///
/// **Wie es geht — und warum es ohne Root geht.** `com.apple.OSDUIHelper` ist
/// ein LaunchAgent in der GUI-Domäne des angemeldeten Benutzers, nicht im
/// System. `launchctl bootout gui/<uid>/…` hängt ihn dort aus. Die Datei selbst
/// liegt unter SIP-Schutz und wird **nicht** angefasst; geändert wird nur, was
/// in der eigenen Sitzung geladen ist. Siehe `docs/spikes/J-osd.md`.
///
/// **Und was das kostet.** Es verschwindet nicht nur die Lautstärkeanzeige,
/// sondern jede: auch Helligkeit und Feststelltaste. Deshalb steht der Schalter
/// standardmäßig aus, und deshalb wird beim Beenden wieder eingehängt — sonst
/// säße jemand bis zum nächsten Abmelden ganz ohne Rückmeldung da, ohne zu
/// wissen, warum.
public enum SystemOSD {
private static let service = "com.apple.OSDUIHelper"
private static var domain: String { "gui/\(getuid())/\(service)" }
/// Ob der Dienst gerade ausgehängt ist.
public static var isSuppressed: Bool {
// `launchctl print` gibt für einen ausgehängten Dienst einen Fehler
// zurück. Kein Fehler heißt: er ist da.
run(["print", domain]) != 0
}
/// Hängt die Einblendung aus. Wirkt bis zum nächsten Abmelden.
@discardableResult
public static func suppress() -> Bool {
guard !isSuppressed else { return true }
let status = run(["bootout", domain])
if status != 0 {
log.notice("OSD-Dienst nicht aushängbar: \(status, privacy: .public)")
}
return status == 0
}
/// Hängt sie wieder ein.
///
/// Beim Beenden zu rufen. Ohne das bliebe der Mac bis zum nächsten Abmelden
/// ohne jede Einblendung — auch für Helligkeit, mit der Onyx nichts zu tun
/// hat.
@discardableResult
public static func restore() -> Bool {
guard isSuppressed else { return true }
let status = run(["bootstrap", "gui/\(getuid())",
"/System/Library/LaunchAgents/\(service).plist"])
if status != 0 {
log.notice("OSD-Dienst nicht einhängbar: \(status, privacy: .public)")
}
return status == 0
}
private static func run(_ arguments: [String]) -> Int32 {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/launchctl")
task.arguments = arguments
task.standardOutput = FileHandle.nullDevice
task.standardError = FileHandle.nullDevice
do {
try task.run()
task.waitUntilExit()
return task.terminationStatus
} catch {
log.error("launchctl nicht startbar: \(error.localizedDescription, privacy: .public)")
return -1
}
}
}