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