Berechtigungsdialog und Einstellungsfenster reparieren

Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon
war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich
hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine
TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der
gar nicht lief.

Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor
showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon
geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt.
Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow.

Kalenderberechtigung, zwei Ursachen übereinander:

Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für
SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete
die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht.
ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll.

Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet
den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist,
an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt
asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start
das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein
echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird.

Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute"
— eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt
jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`.

Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
This commit is contained in:
Guido Schmit
2026-08-10 18:20:21 +02:00
parent 70d4d67cca
commit 40bd83e49e
11 changed files with 774 additions and 141 deletions

View File

@@ -15,15 +15,10 @@ struct OnyxApp: App {
// Onyx hat kein Hauptfenster alles lebt in der Notch und in der
// Menüleiste. Die Einstellungen sind die einzige Ausnahme; im Panel
// wäre dafür zu wenig Platz.
Settings {
if let model = delegate.model {
SettingsView(model: model)
} else {
// Nur sichtbar, wenn die Einzelinstanz-Prüfung uns abgewiesen
// hat und die App gerade wieder verschwindet.
ProgressView().frame(width: 520, height: 420)
}
}
// Eine Szene ist Pflicht, benutzt wird sie nicht: das
// Einstellungsfenster führt SettingsWindowController selbst.
// Siehe die Begründung dort.
Settings { EmptyView() }
}
}
@@ -35,6 +30,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBar: MenuBarController?
private var onyxItem: NSStatusItem?
private var calendarModel: CalendarModel?
private let settingsWindow = SettingsWindowController()
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
@@ -78,6 +74,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
installOnyxStatusItem()
observeMenuBarPreference()
requestPermissions()
}
/// Berechtigungen beim Start anfragen, nicht erst wenn ein Widget sichtbar
/// wird.
///
/// Das Panel ist beim Start zu. Eine an die View gebundene Anfrage kommt
/// deshalb erst, wenn jemand die Notch berührt und bis dahin taucht Onyx
/// in den Systemeinstellungen gar nicht auf, es gibt also nichts, das man
/// dort freischalten könnte. Genau dieser Zustand ist beim Testen
/// aufgetreten.
private func requestPermissions() {
guard let calendarModel else { return }
// Immer einmal laden: mit Berechtigung kommen Termine, ohne sie zeigt
// das Widget den Berechtigungshinweis statt einer leeren Liste.
calendarModel.refresh()
guard !calendarModel.hasAccess else { return }
// Nicht im Hintergrund anfragen.
//
// Onyx ist eine App ohne Fenster. TCC bereitet den Dialog zwar vor
// (promptType 1), entscheidet sich dann aber dagegen (promptPolicy 0),
// weil keine App im Vordergrund ist, an die er gehören könnte. Die
// Anfrage kommt kommentarlos mit `false` zurück, der Status bleibt
// `notDetermined` und in den Systemeinstellungen taucht Onyx nie auf.
// `NSApp.activate` davor hilft nicht: Aktivierung wirkt asynchron und
// ist beim Aufruf noch nicht durch.
//
// Deshalb der ehrliche Weg: beim ersten Start das Einstellungsfenster
// öffnen. Dort ist ein echtes Fenster im Vordergrund, der Dialog
// erscheint zuverlässig und der Nutzer weiß, warum gefragt wird.
showSettings(tab: .permissions)
}
func applicationWillTerminate(_ notification: Notification) {
@@ -140,9 +168,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
@objc private func togglePanel() { coordinator?.togglePanelUnderPointer() }
@objc private func openSettings() {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
@objc private func openSettings() { showSettings(tab: .widgets) }
private func showSettings(tab: SettingsTab) {
guard let model, let calendarModel else { return }
settingsWindow.show(model: model, calendarModel: calendarModel, tab: tab)
}
@objc private func quit() { NSApp.terminate(nil) }