import AirAcceptCore import AppKit import Foundation @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { private let configStore = ConfigStore() private let logger = AirAcceptLogger() private lazy var watcher = AirPlayPromptWatcher(configStore: configStore, logger: logger) private var statusItem: NSStatusItem? private let pollIntervalOptions = [2_000, 3_000, 4_000, 5_000] func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) logger.info("AirAccept launched pid=\(ProcessInfo.processInfo.processIdentifier)") ensureConfigExists() buildMenu() watcher.start() } func applicationWillTerminate(_ notification: Notification) { logger.info("AirAccept terminating pid=\(ProcessInfo.processInfo.processIdentifier)") } private func ensureConfigExists() { do { _ = try configStore.loadOrCreate() } catch { logger.error("failed to create or load config: \(error.localizedDescription)") } } private func buildMenu() { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) if let image = NSImage(systemSymbolName: "airplayvideo", accessibilityDescription: "AirAccept") { image.isTemplate = true item.button?.image = image } else { item.button?.title = "AA" } item.button?.toolTip = "AirAccept" item.menu = makeMenu() statusItem = item } private func makeMenu() -> NSMenu { let menu = NSMenu() let statusItem = NSMenuItem( title: "Status: Running (PID \(ProcessInfo.processInfo.processIdentifier))", action: nil, keyEquivalent: "" ) statusItem.isEnabled = false menu.addItem(statusItem) let permissionStateItem = NSMenuItem( title: AccessibilityReader.isTrusted(prompt: false) ? "Accessibility: Granted" : "Accessibility: Not Granted", action: nil, keyEquivalent: "" ) permissionStateItem.isEnabled = false menu.addItem(permissionStateItem) menu.addItem(.separator()) let watcherItem = NSMenuItem( title: watcher.isEnabled ? "Disable Watcher" : "Enable Watcher", action: #selector(toggleWatcher(_:)), keyEquivalent: "" ) watcherItem.target = self menu.addItem(watcherItem) let intervalMenuItem = NSMenuItem(title: "Poll Interval", action: nil, keyEquivalent: "") intervalMenuItem.submenu = makePollIntervalMenu() menu.addItem(intervalMenuItem) let launchItem = NSMenuItem( title: "Launch at Login", action: #selector(toggleLaunchAtLogin(_:)), keyEquivalent: "" ) launchItem.target = self launchItem.state = LoginItemManager.isEnabled ? .on : .off menu.addItem(launchItem) menu.addItem(.separator()) let permissionItem = NSMenuItem( title: "Request Accessibility Permission", action: #selector(requestAccessibilityPermission(_:)), keyEquivalent: "" ) permissionItem.target = self menu.addItem(permissionItem) let openConfigItem = NSMenuItem( title: "Open Config", action: #selector(openConfig(_:)), keyEquivalent: "" ) openConfigItem.target = self menu.addItem(openConfigItem) let openLogsItem = NSMenuItem( title: "Open Logs", action: #selector(openLogs(_:)), keyEquivalent: "" ) openLogsItem.target = self menu.addItem(openLogsItem) menu.addItem(.separator()) let quitItem = NSMenuItem(title: "Quit", action: #selector(quit(_:)), keyEquivalent: "q") quitItem.target = self menu.addItem(quitItem) return menu } private func makePollIntervalMenu() -> NSMenu { let menu = NSMenu() let currentInterval = currentConfig().pollIntervalMilliseconds for interval in pollIntervalOptions { let seconds = interval / 1_000 let item = NSMenuItem( title: "\(seconds) seconds", action: #selector(setPollInterval(_:)), keyEquivalent: "" ) item.target = self item.representedObject = interval item.state = currentInterval == interval ? .on : .off menu.addItem(item) } return menu } private func rebuildMenu() { statusItem?.menu = makeMenu() } private func currentConfig() -> AirAcceptConfig { (try? configStore.loadOrCreate()) ?? AirAcceptConfig() } @objc private func toggleWatcher(_ sender: NSMenuItem) { watcher.setEnabled(!watcher.isEnabled) rebuildMenu() } @objc private func setPollInterval(_ sender: NSMenuItem) { guard let interval = sender.representedObject as? Int else { return } do { try configStore.save(AirAcceptConfig(pollIntervalMilliseconds: interval)) watcher.reloadConfiguration() logger.info("poll interval set to \(Double(interval) / 1_000.0)s") } catch { logger.error("failed to set poll interval: \(error.localizedDescription)") } rebuildMenu() } @objc private func toggleLaunchAtLogin(_ sender: NSMenuItem) { do { try LoginItemManager.setEnabled(!LoginItemManager.isEnabled) logger.info("launch at login set to \(LoginItemManager.isEnabled)") } catch { logger.error("failed to toggle launch at login: \(error.localizedDescription)") } rebuildMenu() } @objc private func requestAccessibilityPermission(_ sender: NSMenuItem) { _ = AccessibilityReader.isTrusted(prompt: true) } @objc private func openConfig(_ sender: NSMenuItem) { ensureConfigExists() NSWorkspace.shared.activateFileViewerSelecting([configStore.configURL]) } @objc private func openLogs(_ sender: NSMenuItem) { NSWorkspace.shared.activateFileViewerSelecting([logger.logURL]) } @objc private func quit(_ sender: NSMenuItem) { watcher.stop() NSApp.terminate(nil) } }