import AppKit import CoreServices import UserNotifications final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { static var shared: AppDelegate? weak var tabManager: TabManager? weak var notificationStore: TerminalNotificationStore? private var workspaceObserver: NSObjectProtocol? override init() { super.init() Self.shared = self } func applicationDidFinishLaunching(_ notification: Notification) { registerLaunchServicesBundle() enforceSingleInstance() observeDuplicateLaunches() configureUserNotifications() } func configure(tabManager: TabManager, notificationStore: TerminalNotificationStore) { self.tabManager = tabManager self.notificationStore = notificationStore } private func configureUserNotifications() { let actions = [ UNNotificationAction( identifier: TerminalNotificationStore.actionShowIdentifier, title: "Show" ) ] let category = UNNotificationCategory( identifier: TerminalNotificationStore.categoryIdentifier, actions: actions, intentIdentifiers: [], options: [.customDismissAction] ) let center = UNUserNotificationCenter.current() center.setNotificationCategories([category]) center.delegate = self } private func registerLaunchServicesBundle() { let bundleURL = Bundle.main.bundleURL.standardizedFileURL let registerStatus = LSRegisterURL(bundleURL as CFURL, true) if registerStatus != noErr { NSLog("LaunchServices registration failed (status: \(registerStatus)) for \(bundleURL.path)") } } private func enforceSingleInstance() { guard let bundleId = Bundle.main.bundleIdentifier else { return } let currentPid = ProcessInfo.processInfo.processIdentifier let currentURL = Bundle.main.bundleURL.standardizedFileURL for app in NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) { guard app.processIdentifier != currentPid else { continue } if let url = app.bundleURL?.standardizedFileURL, url == currentURL { continue } app.terminate() if !app.isTerminated { _ = app.forceTerminate() } } } private func observeDuplicateLaunches() { guard let bundleId = Bundle.main.bundleIdentifier else { return } let currentPid = ProcessInfo.processInfo.processIdentifier let currentURL = Bundle.main.bundleURL.standardizedFileURL workspaceObserver = NSWorkspace.shared.notificationCenter.addObserver( forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: .main ) { [weak self] notification in guard self != nil else { return } guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } guard app.bundleIdentifier == bundleId, app.processIdentifier != currentPid else { return } if let url = app.bundleURL?.standardizedFileURL, url == currentURL { return } app.terminate() if !app.isTerminated { _ = app.forceTerminate() } NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) } } func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { handleNotificationResponse(response) completionHandler() } func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.banner, .sound, .list]) } private func handleNotificationResponse(_ response: UNNotificationResponse) { guard let tabIdString = response.notification.request.content.userInfo["tabId"] as? String, let tabId = UUID(uuidString: tabIdString) else { return } let surfaceId: UUID? = { guard let surfaceIdString = response.notification.request.content.userInfo["surfaceId"] as? String else { return nil } return UUID(uuidString: surfaceIdString) }() switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier, TerminalNotificationStore.actionShowIdentifier: DispatchQueue.main.async { self.tabManager?.focusTab(tabId, surfaceId: surfaceId) self.markReadIfFocused(response: response, tabId: tabId, surfaceId: surfaceId) } case UNNotificationDismissActionIdentifier: DispatchQueue.main.async { if let notificationId = UUID(uuidString: response.notification.request.identifier) { self.notificationStore?.markRead(id: notificationId) } else if let notificationIdString = response.notification.request.content.userInfo["notificationId"] as? String, let notificationId = UUID(uuidString: notificationIdString) { self.notificationStore?.markRead(id: notificationId) } } default: break } } private func markReadIfFocused(response: UNNotificationResponse, tabId: UUID, surfaceId: UUID?) { let notificationId: UUID? = { if let id = UUID(uuidString: response.notification.request.identifier) { return id } if let idString = response.notification.request.content.userInfo["notificationId"] as? String, let id = UUID(uuidString: idString) { return id } return nil }() guard let notificationId else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { guard let tabManager = self.tabManager else { return } guard tabManager.selectedTabId == tabId else { return } if let surfaceId { guard tabManager.focusedSurfaceId(for: tabId) == surfaceId else { return } } self.notificationStore?.markRead(id: notificationId) } } }