import SwiftUI import AppKit import OnyxCore import OnyxDesign import OnyxNotch import OnyxMenuBar import OnyxWidgetKit import CalendarProvider @main struct OnyxApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate var body: some Scene { // 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. // Eine Szene ist Pflicht, benutzt wird sie nicht: das // Einstellungsfenster führt SettingsWindowController selbst. // Siehe die Begründung dort. Settings { EmptyView() } } } @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { private(set) var model: AppModel? private var coordinator: NotchCoordinator? private var menuBar: MenuBarController? private var onyxItem: NSStatusItem? private var calendarModel: CalendarModel? private let settingsWindow = SettingsWindowController() func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) // Vor allem anderen: sicherstellen, dass wir allein sind. Zwei Instanzen // bringen zwei Zustandsmaschinen und zwei Animationen auf dieselbe // Notch — das sieht aus wie ein Ruckler und ist keiner. if case .blocked(let pids) = SingleInstance.claim() { SingleInstance.explainBlocked(by: pids) NSApp.terminate(nil) return } registerWidgets() let model = AppModel() self.model = model let coordinator = NotchCoordinator(policy: model.displayPolicy) { _, presentation in NotchHostingView( rootView: NotchContainer(presentation: presentation) { NotchPanelContent(model: model) }, presentation: presentation) } coordinator.start() self.coordinator = coordinator applyContentSize() // Einstellungen wirken sofort. Eine Änderung, die erst nach einem // Neustart greift, ist in einer App ohne Fenster praktisch unauffindbar. model.onLayoutChanged = { [weak self] _ in self?.applyContentSize() } model.onDisplayPolicyChanged = { [weak self] policy in self?.coordinator?.policy = policy self?.applyContentSize() } let menuBar = MenuBarController() menuBar.start() self.menuBar = menuBar 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) { coordinator?.stop() menuBar?.stop() } /// Echte Widgets zuerst, danach die Platzhalter für alles, was noch fehlt. private func registerWidgets() { let calendarModel = CalendarModel(source: EventKitSource()) self.calendarModel = calendarModel WidgetRegistry.shared.register(CalendarWidget(model: calendarModel)) PlaceholderWidgets.registerAll() } private func applyContentSize() { guard let model, let coordinator else { return } let size = model.panelContentSize coordinator.activeControllers.forEach { $0.contentSize = size } } // MARK: - Menüleiste /// Das feste Onyx-Element: Panel öffnen, Einstellungen, Beenden. Es ist auch /// das Sicherheitsnetz, falls die Notch einmal nicht reagiert. private func installOnyxStatusItem() { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) item.autosaveName = "onyx.menubar.main" item.button?.image = NSImage(systemSymbolName: "circle.hexagongrid.fill", accessibilityDescription: "Onyx") item.button?.image?.isTemplate = true let menu = NSMenu() menu.addItem(withTitle: String(localized: "menu.openPanel"), action: #selector(togglePanel), keyEquivalent: "").target = self menu.addItem(.separator()) menu.addItem(withTitle: String(localized: "menu.settings"), action: #selector(openSettings), keyEquivalent: ",").target = self menu.addItem(.separator()) menu.addItem(withTitle: String(localized: "menu.quit"), action: #selector(quit), keyEquivalent: "q").target = self item.menu = menu onyxItem = item } private func observeMenuBarPreference() { guard let model else { return } withObservationTracking { _ = model.showsMenuBarIcon } onChange: { Task { @MainActor [weak self] in self?.onyxItem?.isVisible = model.showsMenuBarIcon self?.observeMenuBarPreference() } } onyxItem?.isVisible = model.showsMenuBarIcon } @objc private func togglePanel() { coordinator?.togglePanelUnderPointer() } @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) } } /// Der Inhalt des Panels. Liest `model.layout` und zeichnet sich damit neu, /// sobald in den Einstellungen etwas geändert wird. private struct NotchPanelContent: View { let model: AppModel var body: some View { WidgetGrid(placements: model.layout) } }