From 3aca7224159c3b06d5d1b120b47cab4cd89e33b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Feb 2026 09:50:16 -0800 Subject: [PATCH] macos: further simplication of AppDelegate bell state --- macos/Sources/App/macOS/AppDelegate.swift | 26 +++---------------- .../Terminal/BaseTerminalController.swift | 18 ++++++++++++- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 0e6a8dd2a..dcda887d1 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -151,9 +151,6 @@ class AppDelegate: NSObject, /// Signals private var signals: [DispatchSourceSignal] = [] - /// Current bell state keyed by terminal controller identity. - private var windowBellStates: [ObjectIdentifier: Bool] = [:] - /// Cached permission state for dock badges. private var canShowDockBadgeForBell: Bool = false @@ -234,12 +231,6 @@ class AppDelegate: NSObject, name: NSWindow.didBecomeKeyNotification, object: nil ) - NotificationCenter.default.addObserver( - self, - selector: #selector(windowWillClose), - name: NSWindow.willCloseNotification, - object: nil - ) NotificationCenter.default.addObserver( self, selector: #selector(quickTerminalDidChangeVisibility), @@ -773,14 +764,6 @@ class AppDelegate: NSObject, syncFloatOnTopMenu(notification.object as? NSWindow) } - @objc private func windowWillClose(_ notification: Notification) { - guard let window = notification.object as? NSWindow, - let controller = window.windowController as? BaseTerminalController else { return } - - windowBellStates[ObjectIdentifier(controller)] = nil - syncDockBadgeToTrackedBellState() - } - @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { guard let quickController = notification.object as? QuickTerminalController else { return } self.menuQuickTerminal?.state = if quickController.visible { .on } else { .off } @@ -813,15 +796,14 @@ class AppDelegate: NSObject, } @objc private func terminalWindowHasBell(_ notification: Notification) { - guard let controller = notification.object as? BaseTerminalController, - let hasBell = notification.userInfo?[Notification.Name.terminalWindowHasBellKey] as? Bool else { return } - - windowBellStates[ObjectIdentifier(controller)] = hasBell + guard notification.object is BaseTerminalController else { return } syncDockBadgeToTrackedBellState() } private func syncDockBadgeToTrackedBellState() { - let anyBell = windowBellStates.values.contains(true) + let anyBell = NSApp.windows + .compactMap { $0.windowController as? BaseTerminalController } + .contains { $0.bell } let wantsBadge = ghostty.config.bellFeatures.contains(.attention) && anyBell if wantsBadge && !canShowDockBadgeForBell && !hasRequestedDockBadgeAuthorization { diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ebaf5fd23..51d6a263d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -51,6 +51,9 @@ class BaseTerminalController: NSWindowController, /// Set if the terminal view should show the update overlay. @Published var updateOverlayIsVisible: Bool = false + /// True when any surface in this controller currently has an active bell. + @Published private(set) var bell: Bool = false + /// Whether the terminal surface should focus when the mouse is over it. var focusFollowsMouse: Bool { self.derivedConfig.focusFollowsMouse @@ -137,7 +140,7 @@ class BaseTerminalController: NSWindowController, // Initialize our initial surface. guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } self.surfaceTree = tree ?? .init(view: Ghostty.SurfaceView(ghostty_app, baseConfig: base)) - + // Setup our bell state for the window setupBellNotificationPublisher() @@ -1206,6 +1209,17 @@ class BaseTerminalController: NSWindowController, func windowWillClose(_ notification: Notification) { guard let window else { return } + // Emit a final bell-state transition so any observers can clear state + // without separately tracking NSWindow lifecycle events. + if bell { + bell = false + NotificationCenter.default.post( + name: .terminalWindowBellDidChangeNotification, + object: self, + userInfo: [Notification.Name.terminalWindowHasBellKey: false] + ) + } + // I don't know if this is required anymore. We previously had a ref cycle between // the view and the window so we had to nil this out to break it but I think this // may now be resolved. We should verify that no memory leaks and we can remove this. @@ -1486,8 +1500,10 @@ extension BaseTerminalController { bellStateCancellable = surfaceValuesPublisher(valueKeyPath: \.bell, publisherKeyPath: \.$bell) .map { $0.values.contains(true) } .removeDuplicates() + .receive(on: DispatchQueue.main) .sink { [weak self] hasBell in guard let self else { return } + bell = hasBell NotificationCenter.default.post( name: .terminalWindowBellDidChangeNotification, object: self,