diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c6816d50c..54454e6bf 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -316,6 +316,13 @@ class AppDelegate: NSObject, } } + func applicationWillTerminate(_ notification: Notification) { + // We have no notifications we want to persist after death, + // so remove them all now. In the future we may want to be + // more selective and only remove surface-targeted notifications. + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + } + /// This is called when the application is already open and someone double-clicks the icon /// or clicks the dock icon. func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 0aecef6ad..682efa947 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -306,6 +306,14 @@ extension Ghostty { // We unset our bell state if we gained focus bell = false + + // Remove any notifications for this surface once we gain focus. + if !notificationIdentifiers.isEmpty { + UNUserNotificationCenter.current() + .removeDeliveredNotifications( + withIdentifiers: Array(notificationIdentifiers)) + self.notificationIdentifiers = [] + } } } @@ -1388,13 +1396,29 @@ extension Ghostty { trigger: nil ) - UNUserNotificationCenter.current().add(request) { error in + // Note the callback may be executed on a background thread as documented + // so we need @MainActor since we're reading/writing view state. + UNUserNotificationCenter.current().add(request) { @MainActor error in if let error = error { AppDelegate.logger.error("Error scheduling user notification: \(error)") return } + // We need to keep track of this notification so we can remove it + // under certain circumstances self.notificationIdentifiers.insert(uuid) + + // If we're focused then we schedule to remove the notification + // after a few seconds. If we gain focus we automatically remove it + // in focusDidChange. + if (self.focused) { + Task { @MainActor [weak self] in + try await Task.sleep(for: .seconds(3)) + self?.notificationIdentifiers.remove(uuid) + UNUserNotificationCenter.current() + .removeDeliveredNotifications(withIdentifiers: [uuid]) + } + } } }