From 454a89e011c51c1943400aec2788e1aa544b4ad1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2026 09:59:29 -0800 Subject: [PATCH] macos: clean up badge request state --- macos/Sources/App/macOS/AppDelegate.swift | 98 +++++++++-------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index dcda887d1..199999a91 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -151,12 +151,6 @@ class AppDelegate: NSObject, /// Signals private var signals: [DispatchSourceSignal] = [] - /// Cached permission state for dock badges. - private var canShowDockBadgeForBell: Bool = false - - /// Prevent repeated badge permission prompts. - private var hasRequestedDockBadgeAuthorization: Bool = false - /// The custom app icon image that is currently in use. @Published private(set) var appIcon: NSImage? @@ -339,9 +333,6 @@ class AppDelegate: NSObject, // If we're back manually then clear the hidden state because macOS handles it. self.hiddenState = nil - // Recompute the dock badge based on active terminal bell state. - syncDockBadgeToTrackedBellState() - // First launch stuff if !applicationHasBecomeActive { applicationHasBecomeActive = true @@ -789,67 +780,49 @@ class AppDelegate: NSObject, if ghostty.config.bellFeatures.contains(.attention) { // Bounce the dock icon if we're not focused. NSApp.requestUserAttention(.informationalRequest) - - // Handle setting the dock badge based on permissions - ghosttyUpdateBadgeForBell() } } @objc private func terminalWindowHasBell(_ notification: Notification) { guard notification.object is BaseTerminalController else { return } - syncDockBadgeToTrackedBellState() + syncDockBadge() } - private func syncDockBadgeToTrackedBellState() { - let anyBell = NSApp.windows - .compactMap { $0.windowController as? BaseTerminalController } - .contains { $0.bell } - let wantsBadge = ghostty.config.bellFeatures.contains(.attention) && anyBell - - if wantsBadge && !canShowDockBadgeForBell && !hasRequestedDockBadgeAuthorization { - ghosttyUpdateBadgeForBell() - } - - setDockBadge(wantsBadge && canShowDockBadgeForBell ? "•" : nil) - } - - private func ghosttyUpdateBadgeForBell() { + private func syncDockBadge() { let center = UNUserNotificationCenter.current() center.getNotificationSettings { settings in - DispatchQueue.main.async { - switch settings.authorizationStatus { - case .authorized: - // Already authorized, check badge setting and set if enabled - self.canShowDockBadgeForBell = settings.badgeSetting == .enabled - self.syncDockBadgeToTrackedBellState() - - case .notDetermined: - guard !self.hasRequestedDockBadgeAuthorization else { return } - self.hasRequestedDockBadgeAuthorization = true - - // Not determined yet, request authorization for badge - center.requestAuthorization(options: [.badge]) { granted, error in - if let error = error { - Self.logger.warning("Error requesting badge authorization: \(error)") - return - } - + switch settings.authorizationStatus { + case .authorized: + // If we're authorized and allow badges, then set the badge. + if settings.badgeSetting == .enabled { + DispatchQueue.main.async { + self.setDockBadge() + } + } + + case .notDetermined: + // Not determined yet, request authorization for badge + center.requestAuthorization(options: [.badge]) { granted, error in + if let error = error { + Self.logger.warning("Error requesting badge authorization: \(error)") + return + } + + if granted { + // Permission granted, set the badge DispatchQueue.main.async { - self.canShowDockBadgeForBell = granted - self.syncDockBadgeToTrackedBellState() + self.setDockBadge() } } - - case .denied, .provisional, .ephemeral: - // In these known non-authorized states, do not attempt to set the badge. - self.canShowDockBadgeForBell = false - self.syncDockBadgeToTrackedBellState() - - @unknown default: - // Handle future unknown states by doing nothing. - self.canShowDockBadgeForBell = false - self.syncDockBadgeToTrackedBellState() } + + case .denied, .provisional, .ephemeral: + // In these known non-authorized states, do not attempt to set the badge. + break + + @unknown default: + // Handle future unknown states by doing nothing. + break } } } @@ -874,7 +847,12 @@ class AppDelegate: NSObject, _ = TerminalController.newTab(ghostty, from: window, withBaseConfig: config) } - private func setDockBadge(_ label: String? = "•") { + private func setDockBadge() { + let anyBell = NSApp.windows + .compactMap { $0.windowController as? BaseTerminalController } + .contains { $0.bell } + let wantsBadge = ghostty.config.bellFeatures.contains(.attention) && anyBell + let label = wantsBadge ? "•" : nil NSApp.dockTile.badgeLabel = label NSApp.dockTile.display() } @@ -918,7 +896,9 @@ class AppDelegate: NSObject, // Config could change keybindings, so update everything that depends on that syncMenuShortcuts(config) TerminalController.all.forEach { $0.relabelTabs() } - syncDockBadgeToTrackedBellState() + + // Update our badge since config can change what we show. + syncDockBadge() // Config could change window appearance. We wrap this in an async queue because when // this is called as part of application launch it can deadlock with an internal