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.
This commit is contained in:
Mitchell Hashimoto
2025-06-06 07:22:37 -07:00
parent 08101b0bc5
commit 70f030e3c2
2 changed files with 32 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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])
}
}
}
}