Files
onyx/Packages/OnyxKit/Sources/CalendarProvider/CalendarSource.swift
Guido Schmit 40bd83e49e 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.
2026-08-10 18:20:21 +02:00

94 lines
3.4 KiB
Swift

import Foundation
/// Ein Termin, unabhängig davon, woher er stammt.
public struct OnyxEvent: Identifiable, Equatable, Sendable {
public let id: String
public let title: String
public let start: Date
public let end: Date
public let isAllDay: Bool
/// `#RRGGBB` der Quelle. Kalenderfarben sind die eine Stelle, an der Onyx
/// fremde Farben übernimmt statt eigene zu setzen sonst erkennt man
/// seine Kalender nicht wieder.
public let colorHex: String?
public let location: String?
public let calendarID: String
public let calendarTitle: String
public init(id: String, title: String, start: Date, end: Date, isAllDay: Bool,
colorHex: String?, location: String?,
calendarID: String, calendarTitle: String) {
self.id = id
self.title = title
self.start = start
self.end = end
self.isAllDay = isAllDay
self.colorHex = colorHex
self.location = location
self.calendarID = calendarID
self.calendarTitle = calendarTitle
}
}
public struct OnyxCalendar: Identifiable, Equatable, Sendable {
public let id: String
public let title: String
public let colorHex: String?
public init(id: String, title: String, colorHex: String?) {
self.id = id
self.title = title
self.colorHex = colorHex
}
}
/// Termine **samt der Angabe, wofür überhaupt Daten vorliegen.**
///
/// Das ist der Kern und nicht ein Detail: ein Kalender ohne Termine und ein
/// Kalender, über den nichts bekannt ist, sehen in einer Liste identisch aus
/// nämlich leer. Ein leerer März ist überzeugend und schlicht falsch, wenn die
/// Quelle nur 49 Tage abdeckt. `coverage` trennt beides.
public struct EventWindow: Equatable, Sendable {
public let events: [OnyxEvent]
/// Zeitraum mit gesicherter Information. `nil` bedeutet unbegrenzt
/// so verhält sich der Apple-Kalender, weil er direkt abgefragt wird.
public let coverage: DateInterval?
/// Stand der Daten, falls die Quelle zwischengespeichert liefert.
public let asOf: Date?
public init(events: [OnyxEvent], coverage: DateInterval? = nil, asOf: Date? = nil) {
self.events = events
self.coverage = coverage
self.asOf = asOf
}
public static let empty = EventWindow(events: [])
}
/// Warum gerade keine Termine da sind. Jeder Fall braucht eine eigene Antwort
/// in der Oberfläche nichts anzuzeigen" ist keine.
public enum CalendarSourceState: Equatable, Sendable {
case ready(EventWindow)
/// Berechtigung nicht erteilt.
case denied
/// Die Quelle wurde eingerichtet, hat aber noch nie geschrieben.
case neverWritten
/// Angemeldet sein wäre nötig.
case loggedOut(asOf: Date?)
/// Die Quelle ist neuer als das, was Onyx lesen kann.
case incompatible(found: Int, maxSupported: Int)
case unreadable(reason: String)
}
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
public protocol CalendarSource: Sendable {
var id: String { get }
/// Ob bereits gelesen werden darf **ohne** dabei nachzufragen.
/// Die Einstellungen brauchen das, um den Zustand anzuzeigen, ohne
/// nebenbei einen Systemdialog auszulösen.
var isAuthorized: Bool { get }
func requestAccess() async -> Bool
func calendars() async -> [OnyxCalendar]
func load(_ interval: DateInterval) async -> CalendarSourceState
}