Skip to content

Commit 594ee21

Browse files
authored
macos: defer transparent titlebar KVO rebinding (ghostty-org#13601)
Fixes ghostty-org#13386, based on mustafa0x@a8c090 Defer transparent-titlebar KVO rebinding to the next main-queue turn. Track the observed tab group so unchanged bindings are preserved. Previously, a tab-group callback could invalidate and recreate its own observation before returning, leaving closed terminal windows registered with AppKit after the undo timeout. These windows accumulated titlebar and layer state, increasing memory use and WindowServer CPU with tab churn. Validated with an AppDelegate change that sat and created/closed tabs in a loop, then counted weak controllers/windows/nsapp window.
2 parents 363e6e6 + cfa0ca7 commit 594ee21

1 file changed

Lines changed: 22 additions & 3 deletions

File tree

macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
99
private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig?
1010

1111
/// KVO observation for tab group window changes.
12+
private weak var observedTabGroup: NSWindowTabGroup?
1213
private var tabGroupWindowsObservation: NSKeyValueObservation?
1314
private var tabBarVisibleObservation: NSKeyValueObservation?
1415

@@ -129,9 +130,27 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
129130
// MARK: Tab Group Observation
130131

131132
private func setupKVO() {
132-
// See the docs for the respective setup functions for why.
133-
setupTabGroupObservation()
134-
setupTabBarVisibleObservation()
133+
// This can run from one of the observation callbacks below. Replacing
134+
// an observation before its callback returns leaves the window retained
135+
// by AppKit, so always rebind on the next main-queue turn.
136+
DispatchQueue.main.async { [weak self] in
137+
guard let self else { return }
138+
139+
// Recheck because the tab group and observation state may have changed
140+
// while this work was waiting on the main queue.
141+
let currentTabGroup = self.tabGroup
142+
let observationsValid = currentTabGroup == nil || (
143+
self.tabGroupWindowsObservation != nil &&
144+
self.tabBarVisibleObservation != nil
145+
)
146+
147+
// Keep the existing observations when they already match.
148+
guard self.observedTabGroup !== currentTabGroup || !observationsValid else { return }
149+
150+
self.observedTabGroup = currentTabGroup
151+
self.setupTabGroupObservation()
152+
self.setupTabBarVisibleObservation()
153+
}
135154
}
136155

137156
/// Monitors the tabGroup windows value for any changes and resyncs the appearance on change.

0 commit comments

Comments
 (0)