mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 04:52:47 +00:00
macos: try to clean up Appdelegate combine mess
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
import Sparkle
|
||||
@@ -152,12 +151,6 @@ class AppDelegate: NSObject,
|
||||
/// Signals
|
||||
private var signals: [DispatchSourceSignal] = []
|
||||
|
||||
/// Cancellables used for app-level bell badge tracking.
|
||||
private var bellTrackingCancellables: Set<AnyCancellable> = []
|
||||
|
||||
/// Per-window bell observation cancellables keyed by controller identity.
|
||||
private var windowBellCancellables: [ObjectIdentifier: AnyCancellable] = [:]
|
||||
|
||||
/// Current bell state keyed by terminal controller identity.
|
||||
private var windowBellStates: [ObjectIdentifier: Bool] = [:]
|
||||
|
||||
@@ -241,6 +234,12 @@ 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),
|
||||
@@ -259,6 +258,12 @@ class AppDelegate: NSObject,
|
||||
name: .ghosttyBellDidRing,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(terminalWindowHasBell(_:)),
|
||||
name: .terminalWindowBellDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyNewWindow(_:)),
|
||||
@@ -270,9 +275,6 @@ class AppDelegate: NSObject,
|
||||
name: Ghostty.Notification.ghosttyNewTab,
|
||||
object: nil)
|
||||
|
||||
// Track per-window bell state and keep the dock badge in sync.
|
||||
setupBellBadgeTracking()
|
||||
|
||||
// Configure user notifications
|
||||
let actions = [
|
||||
UNNotificationAction(identifier: Ghostty.userNotificationActionShow, title: "Show")
|
||||
@@ -771,6 +773,14 @@ 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 }
|
||||
@@ -802,57 +812,14 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up observation for all terminal window controllers and aggregates whether any
|
||||
/// associated surface has an active bell.
|
||||
private func setupBellBadgeTracking() {
|
||||
let center = NotificationCenter.default
|
||||
Publishers.MergeMany(
|
||||
center.publisher(for: NSWindow.didBecomeMainNotification).map { _ in () },
|
||||
center.publisher(for: NSWindow.willCloseNotification).map { _ in () }
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.refreshTrackedTerminalWindows()
|
||||
}
|
||||
.store(in: &bellTrackingCancellables)
|
||||
|
||||
refreshTrackedTerminalWindows()
|
||||
ghosttyUpdateBadgeForBell()
|
||||
}
|
||||
|
||||
private func refreshTrackedTerminalWindows() {
|
||||
let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController }
|
||||
let controllersByID = Dictionary(uniqueKeysWithValues: controllers.map { (ObjectIdentifier($0), $0) })
|
||||
let trackedIDs = Set(windowBellCancellables.keys)
|
||||
let currentIDs = Set(controllersByID.keys)
|
||||
|
||||
for id in trackedIDs.subtracting(currentIDs) {
|
||||
windowBellCancellables[id]?.cancel()
|
||||
windowBellCancellables[id] = nil
|
||||
windowBellStates[id] = nil
|
||||
}
|
||||
|
||||
for (id, controller) in controllersByID where windowBellCancellables[id] == nil {
|
||||
windowBellCancellables[id] = makeWindowBellCancellable(controller: controller, id: id)
|
||||
}
|
||||
@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
|
||||
syncDockBadgeToTrackedBellState()
|
||||
}
|
||||
|
||||
private func makeWindowBellCancellable(
|
||||
controller: BaseTerminalController,
|
||||
id: ObjectIdentifier
|
||||
) -> AnyCancellable {
|
||||
controller.surfaceValuesPublisher(valueKeyPath: \.bell, publisherKeyPath: \.$bell)
|
||||
.map { $0.values.contains(true) }
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] hasBell in
|
||||
self?.windowBellStates[id] = hasBell
|
||||
self?.syncDockBadgeToTrackedBellState()
|
||||
}
|
||||
}
|
||||
|
||||
private func syncDockBadgeToTrackedBellState() {
|
||||
let anyBell = windowBellStates.values.contains(true)
|
||||
let wantsBadge = ghostty.config.bellFeatures.contains(.attention) && anyBell
|
||||
|
||||
@@ -83,6 +83,9 @@ class BaseTerminalController: NSWindowController,
|
||||
/// The cancellables related to our focused surface.
|
||||
private var focusedSurfaceCancellables: Set<AnyCancellable> = []
|
||||
|
||||
/// Cancellable for aggregating bell state across all surfaces in this controller.
|
||||
private var bellStateCancellable: AnyCancellable?
|
||||
|
||||
/// An override title for the tab/window set by the user via prompt_tab_title.
|
||||
/// When set, this takes precedence over the computed title from the terminal.
|
||||
var titleOverride: String? {
|
||||
@@ -134,6 +137,9 @@ 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()
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
@@ -222,30 +228,6 @@ class BaseTerminalController: NSWindowController,
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Creates a publisher for values on all surfaces in this controller's tree.
|
||||
///
|
||||
/// The publisher emits a dictionary of surface IDs to values whenever the tree changes
|
||||
/// or any surface publishes a new value for the key path.
|
||||
func surfaceValuesPublisher<Value>(
|
||||
valueKeyPath: KeyPath<Ghostty.SurfaceView, Value>,
|
||||
publisherKeyPath: KeyPath<Ghostty.SurfaceView, Published<Value>.Publisher>
|
||||
) -> AnyPublisher<[Ghostty.SurfaceView.ID: Value], Never> {
|
||||
// `surfaceTree` can be replaced entirely when splits are added/removed/closed.
|
||||
// For each tree snapshot we build a fresh publisher that watches all surfaces
|
||||
// in that snapshot.
|
||||
$surfaceTree
|
||||
.map { tree in
|
||||
tree.valuesPublisher(
|
||||
valueKeyPath: valueKeyPath,
|
||||
publisherKeyPath: publisherKeyPath
|
||||
)
|
||||
}
|
||||
// Keep only the latest tree publisher active. This automatically cancels
|
||||
// subscriptions for old/removed surfaces when the tree changes.
|
||||
.switchToLatest()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Create a new split.
|
||||
@discardableResult
|
||||
func newSplit(
|
||||
@@ -1494,3 +1476,55 @@ extension BaseTerminalController: NSMenuItemValidation {
|
||||
appliedColorScheme = scheme
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Combine Methods
|
||||
|
||||
extension BaseTerminalController {
|
||||
/// Publishes an app-wide notification whenever this terminal window's aggregate
|
||||
/// bell state changes.
|
||||
private func setupBellNotificationPublisher() {
|
||||
bellStateCancellable = surfaceValuesPublisher(valueKeyPath: \.bell, publisherKeyPath: \.$bell)
|
||||
.map { $0.values.contains(true) }
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] hasBell in
|
||||
guard let self else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: .terminalWindowBellDidChangeNotification,
|
||||
object: self,
|
||||
userInfo: [Notification.Name.terminalWindowHasBellKey: hasBell]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a publisher for values on all surfaces in this controller's tree.
|
||||
///
|
||||
/// The publisher emits a dictionary of surface IDs to values whenever the tree changes
|
||||
/// or any surface publishes a new value for the key path.
|
||||
func surfaceValuesPublisher<Value>(
|
||||
valueKeyPath: KeyPath<Ghostty.SurfaceView, Value>,
|
||||
publisherKeyPath: KeyPath<Ghostty.SurfaceView, Published<Value>.Publisher>
|
||||
) -> AnyPublisher<[Ghostty.SurfaceView.ID: Value], Never> {
|
||||
// `surfaceTree` can be replaced entirely when splits are added/removed/closed.
|
||||
// For each tree snapshot we build a fresh publisher that watches all surfaces
|
||||
// in that snapshot.
|
||||
$surfaceTree
|
||||
.map { tree in
|
||||
tree.valuesPublisher(
|
||||
valueKeyPath: valueKeyPath,
|
||||
publisherKeyPath: publisherKeyPath
|
||||
)
|
||||
}
|
||||
// Keep only the latest tree publisher active. This automatically cancels
|
||||
// subscriptions for old/removed surfaces when the tree changes.
|
||||
.switchToLatest()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
extension Notification.Name {
|
||||
/// Terminal window aggregate bell state changed.
|
||||
static let terminalWindowBellDidChangeNotification = Notification.Name("com.mitchellh.ghostty.terminalWindowBellDidChange")
|
||||
static let terminalWindowHasBellKey = terminalWindowBellDidChangeNotification.rawValue + ".hasBell"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user