Ariver
2026-06-10 fe5c1fa0d397938511e6102bbe5562f03f02607f
Fix Quick Switch space mapping fallback
4 files modified
1 files added
198 ■■■■■ changed files
C1.source/Resources/Aligner-Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/Infrastructure/PrivateAPI/SkyLightSpaceEnumerator.swift 93 ●●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchRootView.swift 3 ●●●● patch | view | raw | blame | history
C1.source/Sources/AlignerCore/Spaces/WindowSpaceMappingPolicy.swift 54 ●●●●● patch | view | raw | blame | history
C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift 44 ●●●●● patch | view | raw | blame | history
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>
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
    }
}
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,
C1.source/Sources/AlignerCore/Spaces/WindowSpaceMappingPolicy.swift
New file
@@ -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 }
    }
}
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(