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.
94 lines
3.4 KiB
Swift
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
|
|
}
|