From fe5c1fa0d397938511e6102bbe5562f03f02607f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 10 Jun 2026 16:40:24 +0800
Subject: [PATCH] Fix Quick Switch space mapping fallback

---
 C1.source/Sources/AlignerCore/Spaces/WindowSpaceMappingPolicy.swift               |   54 +++++++++++++
 C1.source/Sources/Aligner/QuickSwitchRootView.swift                               |    3 
 C1.source/Resources/Aligner-Info.plist                                            |    4 
 C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift                           |   44 +++++++++++
 C1.source/Sources/Aligner/Infrastructure/PrivateAPI/SkyLightSpaceEnumerator.swift |   93 +++++++++++++++++++++++
 5 files changed, 195 insertions(+), 3 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 1254b21..6c4daeb 100644
--- a/C1.source/Resources/Aligner-Info.plist
+++ b/C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-    <string>0.0.50</string>
+    <string>0.0.51</string>
 	<key>CFBundleVersion</key>
-    <string>20260610.1156</string>
+    <string>20260610.1637</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>26.0</string>
 	<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/Infrastructure/PrivateAPI/SkyLightSpaceEnumerator.swift b/C1.source/Sources/Aligner/Infrastructure/PrivateAPI/SkyLightSpaceEnumerator.swift
index 3a6065f..b5d9206 100644
--- a/C1.source/Sources/Aligner/Infrastructure/PrivateAPI/SkyLightSpaceEnumerator.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/PrivateAPI/SkyLightSpaceEnumerator.swift
@@ -25,11 +25,20 @@
     private typealias CGSMainConnectionIDFunction = @convention(c) () -> UInt32
     private typealias CGSCopyManagedDisplaySpacesFunction = @convention(c) (UInt32) -> Unmanaged<CFArray>?
     private typealias CGSCopySpacesForWindowsFunction = @convention(c) (UInt32, UInt32, CFArray) -> Unmanaged<CFArray>?
+    private typealias CGSCopyWindowsWithOptionsAndTagsFunction = @convention(c) (
+        UInt32,
+        Int,
+        CFArray,
+        Int,
+        UnsafeMutablePointer<Int>,
+        UnsafeMutablePointer<Int>
+    ) -> Unmanaged<CFArray>?
 
     private let handle: UnsafeMutableRawPointer?
     private let mainConnectionID: CGSMainConnectionIDFunction
     private let copyManagedDisplaySpaces: CGSCopyManagedDisplaySpacesFunction
     private let copySpacesForWindows: CGSCopySpacesForWindowsFunction?
+    private let copyWindowsWithOptionsAndTags: CGSCopyWindowsWithOptionsAndTagsFunction?
 
     init() throws {
         guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY) else {
@@ -40,6 +49,7 @@
         self.mainConnectionID = try Self.symbol("CGSMainConnectionID", in: handle)
         self.copyManagedDisplaySpaces = try Self.symbol("CGSCopyManagedDisplaySpaces", in: handle)
         self.copySpacesForWindows = try? Self.symbol("CGSCopySpacesForWindows", in: handle)
+        self.copyWindowsWithOptionsAndTags = try? Self.symbol("CGSCopyWindowsWithOptionsAndTags", in: handle)
     }
 
     deinit {
@@ -115,6 +125,27 @@
     }
 
     func spaceIDsByWindowID(windowIDs: [UInt32]) -> [UInt32: [UInt64]] {
+        let directSpaceIDsByWindowID = directSpaceIDsByWindowID(windowIDs: windowIDs)
+        guard let orderedSpaceIDs = try? allManagedSpaceIDs(),
+              !orderedSpaceIDs.isEmpty
+        else {
+            return directSpaceIDsByWindowID
+        }
+
+        let windowIDsBySpaceID = windowIDsBySpaceID(spaceIDs: orderedSpaceIDs)
+        guard !windowIDsBySpaceID.isEmpty else {
+            return directSpaceIDsByWindowID
+        }
+
+        return WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
+            requestedWindowIDs: windowIDs,
+            directSpaceIDsByWindowID: directSpaceIDsByWindowID,
+            windowIDsBySpaceID: windowIDsBySpaceID,
+            orderedSpaceIDs: orderedSpaceIDs
+        )
+    }
+
+    private func directSpaceIDsByWindowID(windowIDs: [UInt32]) -> [UInt32: [UInt64]] {
         guard let copySpacesForWindows else { return [:] }
 
         let connectionID = mainConnectionID()
@@ -138,6 +169,53 @@
 
         return result
     }
+
+    private func allManagedSpaceIDs() throws -> [UInt64] {
+        try managedDisplaySpaceRecords()
+            .flatMap { displayRecord -> [UInt64] in
+                let spaceRecords = displayRecord["Spaces"] as? [[String: Any]] ?? []
+                return spaceRecords.compactMap { spaceRecord in
+                    uint64Value(spaceRecord["id64"] ?? spaceRecord["id"])
+                }
+            }
+    }
+
+    private func windowIDsBySpaceID(spaceIDs: [UInt64]) -> [UInt64: [UInt32]] {
+        guard let copyWindowsWithOptionsAndTags else { return [:] }
+
+        let connectionID = mainConnectionID()
+        let options = Self.copyWindowsOptionIncludeInvisible
+        var result: [UInt64: [UInt32]] = [:]
+
+        for spaceID in spaceIDs {
+            let spaceIDArray = [NSNumber(value: spaceID)] as CFArray
+            var setTags = 0
+            var clearTags = 0
+            guard let unmanagedWindows = copyWindowsWithOptionsAndTags(
+                connectionID,
+                0,
+                spaceIDArray,
+                options,
+                &setTags,
+                &clearTags
+            ) else {
+                continue
+            }
+
+            let windowIDs = (unmanagedWindows.takeRetainedValue() as? [Any] ?? [])
+                .compactMap { item -> UInt32? in
+                    uint32Value(item)
+                }
+
+            if !windowIDs.isEmpty {
+                result[spaceID] = windowIDs
+            }
+        }
+
+        return result
+    }
+
+    private static let copyWindowsOptionIncludeInvisible = (1 << 0) | (1 << 1) | (1 << 2)
 
     private static func symbol<T>(_ name: String, in handle: UnsafeMutableRawPointer) throws -> T {
         guard let rawSymbol = dlsym(handle, name) else {
@@ -217,3 +295,18 @@
         return nil
     }
 }
+
+private func uint32Value(_ value: Any?) -> UInt32? {
+    switch value {
+    case let number as NSNumber:
+        return number.uint32Value
+    case let uint32 as UInt32:
+        return uint32
+    case let uint as UInt where uint <= UInt(UInt32.max):
+        return UInt32(uint)
+    case let int as Int where int >= 0 && int <= Int(UInt32.max):
+        return UInt32(int)
+    default:
+        return nil
+    }
+}
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index f3e63f2..3e34cf5 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -722,7 +722,8 @@
             "waterfallMaxScrollOffset": Double(waterfallMaxScrollOffset),
             "waterfallAlignmentMinScrollOffset": Double(waterfallAlignmentScrollRange.lowerBound),
             "waterfallAlignmentMaxScrollOffset": Double(waterfallAlignmentScrollRange.upperBound),
-            "waterfallScrollable": waterfallAlignmentScrollable,
+            "waterfallScrollable": waterfallMaxScrollOffset > 0,
+            "waterfallAlignmentScrollable": waterfallAlignmentScrollable,
             "waterfallColumnCount": waterfallColumns.count,
             "waterfallColumnNames": waterfallColumns.map(\.column.app.name),
             "waterfallClipsToBounds": waterfallLayer.masksToBounds,
diff --git a/C1.source/Sources/AlignerCore/Spaces/WindowSpaceMappingPolicy.swift b/C1.source/Sources/AlignerCore/Spaces/WindowSpaceMappingPolicy.swift
new file mode 100644
index 0000000..7306f1d
--- /dev/null
+++ b/C1.source/Sources/AlignerCore/Spaces/WindowSpaceMappingPolicy.swift
@@ -0,0 +1,54 @@
+import Foundation
+
+public enum WindowSpaceMappingPolicy {
+    public static func mergedSpaceIDsByWindowID(
+        requestedWindowIDs: [UInt32],
+        directSpaceIDsByWindowID: [UInt32: [UInt64]],
+        windowIDsBySpaceID: [UInt64: [UInt32]],
+        orderedSpaceIDs: [UInt64]
+    ) -> [UInt32: [UInt64]] {
+        let requestedWindowIDSet = Set(requestedWindowIDs)
+        var result: [UInt32: [UInt64]] = [:]
+
+        for windowID in requestedWindowIDs {
+            let directSpaceIDs = orderedUnique(directSpaceIDsByWindowID[windowID] ?? [])
+            if !directSpaceIDs.isEmpty {
+                result[windowID] = directSpaceIDs
+            }
+        }
+
+        for spaceID in normalizedSpaceOrder(
+            orderedSpaceIDs: orderedSpaceIDs,
+            windowIDsBySpaceID: windowIDsBySpaceID
+        ) {
+            for windowID in windowIDsBySpaceID[spaceID] ?? [] {
+                guard requestedWindowIDSet.contains(windowID),
+                      result[windowID] == nil
+                else {
+                    continue
+                }
+
+                result[windowID, default: []].append(spaceID)
+            }
+        }
+
+        return result.mapValues(orderedUnique)
+    }
+
+    private static func normalizedSpaceOrder(
+        orderedSpaceIDs: [UInt64],
+        windowIDsBySpaceID: [UInt64: [UInt32]]
+    ) -> [UInt64] {
+        let orderedUniqueSpaceIDs = orderedUnique(orderedSpaceIDs)
+        if !orderedUniqueSpaceIDs.isEmpty {
+            return orderedUniqueSpaceIDs
+        }
+
+        return windowIDsBySpaceID.keys.sorted()
+    }
+
+    private static func orderedUnique<T: Hashable>(_ values: [T]) -> [T] {
+        var seen = Set<T>()
+        return values.filter { seen.insert($0).inserted }
+    }
+}
diff --git a/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift b/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift
index 298f686..a6c3447 100644
--- a/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift
+++ b/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift
@@ -1174,6 +1174,50 @@
         XCTAssertEqual(snapshot.appGroups.single?.windows.map(\.primarySpaceID), [10, 11, 12, 12, nil])
     }
 
+    func testWindowSpaceMappingFillsUnresolvedWindowsFromSpaceInverseMap() {
+        let mapping = WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
+            requestedWindowIDs: [10, 20, 30],
+            directSpaceIDsByWindowID: [10: [100]],
+            windowIDsBySpaceID: [
+                100: [10, 20],
+                101: [30]
+            ],
+            orderedSpaceIDs: [100, 101]
+        )
+
+        XCTAssertEqual(mapping[10], [100])
+        XCTAssertEqual(mapping[20], [100])
+        XCTAssertEqual(mapping[30], [101])
+    }
+
+    func testWindowSpaceMappingDoesNotOverwriteDirectSpaceAssignments() {
+        let mapping = WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
+            requestedWindowIDs: [10],
+            directSpaceIDsByWindowID: [10: [101]],
+            windowIDsBySpaceID: [
+                100: [10]
+            ],
+            orderedSpaceIDs: [100, 101]
+        )
+
+        XCTAssertEqual(mapping[10], [101])
+    }
+
+    func testWindowSpaceMappingLeavesAmbiguousNoSpaceWindowsUnassigned() {
+        let mapping = WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
+            requestedWindowIDs: [10, 20],
+            directSpaceIDsByWindowID: [:],
+            windowIDsBySpaceID: [
+                100: [10, 99]
+            ],
+            orderedSpaceIDs: [100]
+        )
+
+        XCTAssertEqual(mapping[10], [100])
+        XCTAssertNil(mapping[20])
+        XCTAssertNil(mapping[99])
+    }
+
     func testQuickSwitchSnapshotKeepsMinimizedAndFullscreenWindows() {
         let app = makeApp()
         let snapshot = QuickSwitchSnapshotBuilder.snapshot(

--
Gitblit v1.9.3