Skip to content

Commit 060e896

Browse files
author
Eden Rochman
committed
Improve tab detection reliability
- Add appWindowCount safety check: only consider windows as tabs if the app has 2+ windows known to AeroSpace, preventing false positives when CGWindowList is slow to update - Simplify normalization: split into validatePopups() and demoteInactiveTabs() for clearer logic - Refresh CG cache once per pass for consistency - Add refreshNativeTabDetection() and windowCountForApp() helpers
1 parent 0c7c8e4 commit 060e896

File tree

3 files changed

+47
-28
lines changed

3 files changed

+47
-28
lines changed

Sources/AppBundle/normalizeLayoutReason.swift

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,39 @@ func normalizeLayoutReason() async throws {
55
try await _normalizeLayoutReason(workspace: workspace, windows: windows)
66
}
77
try await _normalizeLayoutReason(workspace: focus.workspace, windows: macosMinimizedWindowsContainer.children.filterIsInstance(of: Window.self))
8-
try await demoteNativeTabsToPopup()
9-
try await validateStillPopups()
8+
try await validatePopups()
9+
demoteInactiveTabs()
1010
}
1111

12-
/// Move tiled windows that have become inactive native tabs to the popup container.
13-
/// This handles the case where a tiled window becomes a background tab after the user opens a new tab.
12+
/// Promote popup windows that are actually real windows (or newly active tabs).
1413
/// https://github.com/nikitabobko/AeroSpace/issues/68
1514
@MainActor
16-
private func demoteNativeTabsToPopup() async throws {
17-
for workspace in Workspace.all {
18-
for window in workspace.allLeafWindowsRecursive {
19-
guard let macWindow = window as? MacWindow else { continue }
20-
if isLikelyNativeTab(windowId: macWindow.windowId, appPid: macWindow.macApp.pid) {
21-
macWindow.bind(to: macosPopupWindowsContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
22-
}
15+
private func validatePopups() async throws {
16+
refreshNativeTabDetection()
17+
for node in Array(macosPopupWindowsContainer.children) {
18+
guard let popup = node as? MacWindow else { continue }
19+
// Don't promote inactive native tabs
20+
if isLikelyNativeTab(windowId: popup.windowId, appPid: popup.macApp.pid, appWindowCount: windowCountForApp(pid: popup.macApp.pid)) { continue }
21+
// This window is on-screen and should be promoted to tiling
22+
let windowLevel = getWindowLevel(for: popup.windowId)
23+
if try await popup.isWindowHeuristic(windowLevel) {
24+
try await popup.relayoutWindow(on: focus.workspace)
25+
try await tryOnWindowDetected(popup)
2326
}
2427
}
2528
}
2629

30+
/// Demote tiled windows that have become inactive native tabs to popup container.
31+
/// https://github.com/nikitabobko/AeroSpace/issues/68
2732
@MainActor
28-
private func validateStillPopups() async throws {
29-
for node in macosPopupWindowsContainer.children {
30-
let popup = (node as! MacWindow)
31-
// Don't promote native tabs back to tiling — they were intentionally placed in popup container
32-
// https://github.com/nikitabobko/AeroSpace/issues/68
33-
if isLikelyNativeTab(windowId: popup.windowId, appPid: popup.macApp.pid) {
34-
continue
35-
}
36-
let windowLevel = getWindowLevel(for: popup.windowId)
37-
if try await popup.isWindowHeuristic(windowLevel) {
38-
try await popup.relayoutWindow(on: focus.workspace)
39-
try await tryOnWindowDetected(popup)
33+
private func demoteInactiveTabs() {
34+
refreshNativeTabDetection()
35+
for workspace in Workspace.all {
36+
for window in Array(workspace.allLeafWindowsRecursive) {
37+
guard let macWindow = window as? MacWindow else { continue }
38+
if isLikelyNativeTab(windowId: macWindow.windowId, appPid: macWindow.macApp.pid, appWindowCount: windowCountForApp(pid: macWindow.macApp.pid)) {
39+
macWindow.bind(to: macosPopupWindowsContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
40+
}
4041
}
4142
}
4243
}

Sources/AppBundle/tree/MacWindow.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ private func unbindAndGetBindingDataForNewWindow(_ windowId: UInt32, _ macApp: M
216216
// Tab detection heuristic: if a window is not on screen but the same app has an
217217
// on-screen window, it's likely an inactive macOS native tab.
218218
// https://github.com/nikitabobko/AeroSpace/issues/68
219-
if isLikelyNativeTab(windowId: windowId, appPid: macApp.pid) {
219+
refreshNativeTabDetection()
220+
if isLikelyNativeTab(windowId: windowId, appPid: macApp.pid, appWindowCount: windowCountForApp(pid: macApp.pid)) {
220221
return BindingData(parent: macosPopupWindowsContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
221222
}
222223

Sources/AppBundle/windowLevelCache.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,38 @@ func getWindowLevel(for windowId: UInt32) -> MacOsWindowLevel? {
5454
return levelCache[windowId]
5555
}
5656

57+
/// Refresh the CG cache once, then use isLikelyNativeTab for consistent results within a single pass.
58+
@MainActor
59+
func refreshNativeTabDetection() {
60+
refreshCgWindowInfoCache()
61+
}
62+
63+
/// Count how many windows AeroSpace knows about for a given app PID.
64+
/// Includes tiled, floating, and popup windows.
65+
@MainActor
66+
func windowCountForApp(pid: pid_t) -> Int {
67+
MacWindow.allWindows.count(where: { $0.macApp.pid == pid })
68+
}
69+
5770
/// Detect macOS native tabs: the AX API reports tabs as separate windows, but only the active
5871
/// tab appears in CGWindowListCopyWindowInfo(.optionOnScreenOnly). If a window is NOT on screen
5972
/// but another window from the same app IS on screen, it's likely an inactive native tab.
6073
/// https://github.com/nikitabobko/AeroSpace/issues/68
74+
///
75+
/// Additional safety: only consider a window as a tab if the same app has at least one OTHER
76+
/// window on-screen with the same PID. This prevents false positives when CG is slow to update.
6177
@MainActor
62-
func isLikelyNativeTab(windowId: UInt32, appPid: pid_t) -> Bool {
63-
refreshCgWindowInfoCache()
78+
func isLikelyNativeTab(windowId: UInt32, appPid: pid_t, appWindowCount: Int) -> Bool {
79+
// If the app only has 1 window known to AeroSpace, it can't be a tab
80+
if appWindowCount <= 1 { return false }
6481

6582
// If this window IS on screen, it's either a real window or the active tab — tile it normally.
6683
if cgWindowInfoCache[windowId] != nil { return false }
6784

6885
// This window is NOT on screen. Check if the same app has at least one normal window on screen.
6986
// If so, this off-screen window is likely an inactive native tab.
70-
for (_, info) in cgWindowInfoCache {
71-
if info.ownerPid == appPid && info.level == .normalWindow {
87+
for (otherId, info) in cgWindowInfoCache {
88+
if otherId != windowId && info.ownerPid == appPid && info.level == .normalWindow {
7289
return true
7390
}
7491
}

0 commit comments

Comments
 (0)