Fix notification focus/read handling and add split surface control

This commit is contained in:
Lawrence Chen 2026-01-22 18:58:04 -08:00
parent bc074d20c1
commit 5acb4e47b1
6 changed files with 384 additions and 17 deletions

View file

@ -1,4 +1,5 @@
import AppKit
import CoreServices
import UserNotifications
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
@ -6,6 +7,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
weak var tabManager: TabManager?
weak var notificationStore: TerminalNotificationStore?
private var workspaceObserver: NSObjectProtocol?
override init() {
super.init()
@ -13,6 +15,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
func applicationDidFinishLaunching(_ notification: Notification) {
registerLaunchServicesBundle()
enforceSingleInstance()
observeDuplicateLaunches()
configureUserNotifications()
}
@ -41,6 +46,52 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
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,
@ -55,7 +106,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound])
completionHandler([.banner, .sound, .list])
}
private func handleNotificationResponse(_ response: UNNotificationResponse) {
@ -73,13 +124,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier, TerminalNotificationStore.actionShowIdentifier:
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)
}
self.tabManager?.focusTab(tabId, surfaceId: surfaceId)
self.markReadIfFocused(response: response, tabId: tabId, surfaceId: surfaceId)
}
case UNNotificationDismissActionIdentifier:
DispatchQueue.main.async {
@ -95,4 +141,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
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)
}
}
}