From 70f030e3c2f09ad7785846b94c40988dcc97266c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 07:22:37 -0700 Subject: [PATCH] macos: dismiss notifications on focus, application exit I've only recently been using programs that use user notifications heavily and this commit addresses a number of annoyances I've encountered. 1. Notifications dispatched while the source terminal surface is focused are now only shown for a short time (3 seconds hardcoded) and then automatically dismiss. 2. Notifications are dismissed when the target surface becomes focused from an unfocused state. This dismissal happens immediately (no delay). 3. Notifications are dismissed when the application exits. 4. This fixes a bug where notification callbacks were modifying view state, but the notification center doesn't guarantee that the callback is called on the main thread. We now ensure that the callback is always called on the main thread. --- macos/Sources/App/macOS/AppDelegate.swift | 7 +++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 26 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) 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]) + } + } } }