| C1.source/Resources/Aligner-Info.plist | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/AlignerApplicationDelegate.swift | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/QuickSwitchRootView.swift | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/QuickSwitchSessionController.swift | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/SpaceActivationService.swift | ●●●●● patch | view | raw | blame | history | |
| C3.tools/round1-space-filter-fixture-qa.sh | ●●●●● 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.53</string> <string>0.0.54</string> <key>CFBundleVersion</key> <string>20260611.1607</string> <string>20260611.1947</string> <key>LSMinimumSystemVersion</key> <string>26.0</string> <key>NSHighResolutionCapable</key> C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -354,6 +354,7 @@ let controller = QuickSwitchSessionController( snapshotLoader: quickSwitchSnapshotLoader(), windowActivationService: quickSwitchWindowActivationService(), spaceActivationService: quickSwitchSpaceActivationService(), windowCloseService: quickSwitchWindowCloseService(), disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh, closeConfirmationRequired: quickSwitchConfirmBeforeClose, @@ -428,6 +429,7 @@ let controller = QuickSwitchSessionController( snapshotLoader: quickSwitchSnapshotLoader(), windowActivationService: quickSwitchWindowActivationService(), spaceActivationService: quickSwitchSpaceActivationService(), windowCloseService: quickSwitchWindowCloseService(), disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh, closeConfirmationRequired: quickSwitchConfirmBeforeClose, @@ -546,6 +548,14 @@ return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] }) } private func quickSwitchSpaceActivationService() -> any SpaceActivationServiceProtocol { if round1QuickSwitchOptions.debugWindowActivation { return DebugSpaceActivationService() } return PrivateSpaceActivationService() } private func quickSwitchWindowCloseService() -> any WindowCloseServiceProtocol { if round1QuickSwitchOptions.debugWindowClose { return DebugWindowCloseService() C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -82,9 +82,12 @@ private var lastProjectionTransitionFadeInLayerCount = 0 private var lastProjectionTransitionFadeOutLayerCount = 0 private var lastProjectionTransitionDurationMilliseconds: Double = 0 private var suppressEventInput = false private var suppressEventBackgroundClicks = false var onEscape: (() -> Void)? var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)? var onSpaceLaneClick: ((UInt64) -> Void)? var onSpaceLaneClick: ((UInt64, Int) -> Void)? var onBackgroundClick: (() -> Bool)? var onRequestClose: ((QuickSwitchCloseRequest) -> Void)? var onCloseConfirmationDisabled: (() -> Void)? @@ -379,6 +382,13 @@ } override func keyDown(with event: NSEvent) { if suppressEventInput { DevelopmentDiagnostics.log("quickSwitch.view.keyDown.ignoredForDebugSequence", [ "keyCode": event.keyCode ]) return } switch event.keyCode { case TriggerKeyCode.escape where Self.shouldDismissOnEscape(event): onEscape?() @@ -462,6 +472,15 @@ } override func mouseDown(with event: NSEvent) { if suppressEventInput { DevelopmentDiagnostics.log("quickSwitch.view.mouseDown.ignoredForDebugSequence", [ "x": event.locationInWindow.x, "y": event.locationInWindow.y, "bounds": NSStringFromRect(bounds) ]) return } if handleCloseConfirmationMouseDown(at: event.locationInWindow) { return } @@ -477,7 +496,7 @@ } if let segment = spaceLaneSegment(at: event.locationInWindow) { clickSpaceLaneSegment(segment) clickSpaceLaneSegment(segment, clickCount: max(1, event.clickCount)) return } @@ -491,6 +510,9 @@ "y": event.locationInWindow.y, "bounds": NSStringFromRect(bounds) ]) if !suppressEventBackgroundClicks, onBackgroundClick?() == true { return } super.mouseDown(with: event) } @@ -532,6 +554,15 @@ break } } } func setSuppressEventBackgroundClicks(_ suppressed: Bool) { suppressEventBackgroundClicks = suppressed } func setSuppressEventInput(_ suppressed: Bool) { suppressEventInput = suppressed suppressEventBackgroundClicks = suppressed } func beginPerformanceFirstFrameMeasurement() { @@ -1207,8 +1238,21 @@ } recordMouseCommand("click-space:\(spaceID)") if let segment = spaceLaneSegment(spaceID: spaceID) { clickSpaceLaneSegment(segment) clickSpaceLaneSegment(segment, clickCount: 1) } case "double-click-space" where parts.count == 2: guard let spaceID = UInt64(parts[1]) else { recordMouseCommand("invalid:\(command)") return } recordMouseCommand("double-click-space:\(spaceID)") if let segment = spaceLaneSegment(spaceID: spaceID) { clickSpaceLaneSegment(segment, clickCount: 1) clickSpaceLaneSegment(segment, clickCount: 2) } case "click-background": recordMouseCommand("click-background") _ = onBackgroundClick?() case "hover-card" where parts.count == 3: guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else { recordMouseCommand("invalid:\(command)") @@ -1324,13 +1368,14 @@ focusSpace(segment.space.id, persistent: false, source: source) } private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) { private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers, clickCount: Int = 1) { DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane", [ "spaceID": segment.space.id, "label": segment.space.label "label": segment.space.label, "clickCount": clickCount ]) lastSpaceLaneClickSpaceID = segment.space.id onSpaceLaneClick?(segment.space.id) onSpaceLaneClick?(segment.space.id, clickCount) lastSpaceLaneClickSpaceID = segment.space.id } @@ -2443,9 +2488,11 @@ let isActive = isHovered || isFocused || isLocked let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1 segment.containerLayer.borderWidth = isLocked ? 2.75 : (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1 segment.containerLayer.borderColor = (isActive ? spaceLaneOccupiedColor.withAlphaComponent(0.62) ? spaceLaneOccupiedColor.withAlphaComponent(isLocked ? 0.92 : 0.62) : isAssociated ? spaceLaneOccupiedColor.withAlphaComponent(0.50) : hasWindows @@ -2472,17 +2519,20 @@ colors: spaceLaneIdleOccupiedGradientColors() ) } segment.containerLayer.shadowColor = isLocked ? NSColor.white.withAlphaComponent(0.98).cgColor : NSColor.black.cgColor segment.containerLayer.shadowOpacity = (hasWindows || isLocked) ? (isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0) ? (isLocked ? 0.72 : isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0) : 0 segment.containerLayer.shadowRadius = (hasWindows || isLocked) ? (isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0) ? (isLocked ? 30 : isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0) : 0 segment.containerLayer.shadowOffset = CGSize( width: 0, height: (hasWindows || isLocked) ? (isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0 height: (hasWindows || isLocked) ? (isLocked ? 0 : isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0 ) segment.containerLayer.zPosition = isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0 segment.containerLayer.zPosition = isLocked ? 42 : isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0 segment.containerLayer.transform = (isActive || isAssociated) ? CATransform3DMakeScale(isActive ? 1.03 : 1.015, isActive ? 1.03 : 1.015, 1) : CATransform3DIdentity @@ -4426,8 +4476,13 @@ "gradientColors": gradientColorDictionaries(from: segment.containerLayer), "borderColor": colorDictionary(from: segment.containerLayer.borderColor), "borderWidth": Double(segment.containerLayer.borderWidth), "shadowColor": colorDictionary(from: segment.containerLayer.shadowColor), "shadowOpacity": Double(segment.containerLayer.shadowOpacity), "shadowRadius": Double(segment.containerLayer.shadowRadius), "shadowOffset": [ "width": Double(segment.containerLayer.shadowOffset.width), "height": Double(segment.containerLayer.shadowOffset.height) ], "appFrame": dictionary(from: segment.appLayer.frame), "frame": dictionary(from: segment.containerLayer.frame), "visibleFrame": dictionary(from: visibleFrame), C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -15,6 +15,7 @@ private let snapshotLoader: any QuickSwitchSnapshotLoading private let screenshotProvider: any ScreenshotProviderProtocol private let windowActivationService: any WindowActivationServiceProtocol private let spaceActivationService: any SpaceActivationServiceProtocol private let windowCloseService: any WindowCloseServiceProtocol private let systemCriticalWindowDetector: any SystemCriticalWindowDetecting private let disableScreenshotRefresh: Bool @@ -35,6 +36,8 @@ private var lockedSpaceFilterID: UInt64? private var selectionBeforeSpaceFilter: QuickSwitchSelection? private var lastSpaceFilterAction: String? private var pendingLockedSpaceUnlockSpaceID: UInt64? private var pendingLockedSpaceUnlockTask: Task<Void, Never>? private var lastSnapshotError: String? private var snapshotTask: Task<Void, Never>? private var screenshotTask: Task<Void, Never>? @@ -52,6 +55,11 @@ private var lastActivationWindowID: UInt32? private var lastActivationResult: WindowActivationResult? private var lastActivationError: String? private var lastSpaceActivationSpaceID: UInt64? private var lastSpaceActivationDidRequestFocus: Bool? private var lastSpaceActivationDisplayIdentifier: String? private var lastSpaceActivationPreviousCurrentSpaceID: UInt64? private var lastSpaceActivationError: String? private var lastCloseTargetKind: String? private var lastCloseAppGroupIndex: Int? private var lastCloseAppName: String? @@ -106,6 +114,7 @@ windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService( spaceIDsByWindowIDProvider: { _ in [:] } ), spaceActivationService: any SpaceActivationServiceProtocol = PrivateSpaceActivationService(), windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService( spaceIDsByWindowIDProvider: { _ in [:] } ), @@ -122,6 +131,7 @@ self.snapshotLoader = snapshotLoader self.screenshotProvider = screenshotProvider self.windowActivationService = windowActivationService self.spaceActivationService = spaceActivationService self.windowCloseService = windowCloseService self.systemCriticalWindowDetector = systemCriticalWindowDetector self.disableScreenshotRefresh = disableScreenshotRefresh @@ -155,6 +165,11 @@ lastActivationWindowID = nil lastActivationResult = nil lastActivationError = nil lastSpaceActivationSpaceID = nil lastSpaceActivationDidRequestFocus = nil lastSpaceActivationDisplayIdentifier = nil lastSpaceActivationPreviousCurrentSpaceID = nil lastSpaceActivationError = nil lastCloseTargetKind = nil lastCloseAppGroupIndex = nil lastCloseAppName = nil @@ -163,6 +178,7 @@ lastCloseError = nil pendingCloseVerification = nil suppressedCloseTargets = [] cancelPendingLockedSpaceUnlock() lockedSpaceFilterID = nil selectionBeforeSpaceFilter = nil lastSpaceFilterAction = nil @@ -187,9 +203,13 @@ view.onCommitSelection = { [weak self] selection, source in self?.commitSelection(selection, source: source) } view.onSpaceLaneClick = { [weak self] spaceID in self?.handleSpaceLaneClick(spaceID) view.onSpaceLaneClick = { [weak self] spaceID, clickCount in self?.handleSpaceLaneClick(spaceID, clickCount: clickCount) } view.onBackgroundClick = { [weak self] in self?.handleBackgroundClick() ?? false } view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty) view.onRequestClose = { [weak self] request in self?.requestClose(request) } @@ -228,6 +248,7 @@ snapshotTask = nil screenshotTask?.cancel() screenshotTask = nil cancelPendingLockedSpaceUnlock() pendingCloseVerification = nil view.clearCloseFeedback() invalidateSystemCriticalMonitoring() @@ -288,6 +309,11 @@ "lastActivationWindowID": lastActivationWindowID ?? NSNull(), "lastActivationResult": lastActivationResult.map(Self.activationResultString) ?? NSNull(), "lastActivationError": lastActivationError ?? NSNull(), "lastSpaceActivationSpaceID": lastSpaceActivationSpaceID ?? NSNull(), "lastSpaceActivationDidRequestFocus": lastSpaceActivationDidRequestFocus ?? NSNull(), "lastSpaceActivationDisplayIdentifier": lastSpaceActivationDisplayIdentifier ?? NSNull(), "lastSpaceActivationPreviousCurrentSpaceID": lastSpaceActivationPreviousCurrentSpaceID ?? NSNull(), "lastSpaceActivationError": lastSpaceActivationError ?? NSNull(), "lastCloseTargetKind": lastCloseTargetKind ?? NSNull(), "lastCloseAppGroupIndex": lastCloseAppGroupIndex ?? NSNull(), "lastCloseAppName": lastCloseAppName ?? NSNull(), @@ -534,7 +560,31 @@ onSnapshotUpdated?() } private func handleSpaceLaneClick(_ spaceID: UInt64) { private func handleSpaceLaneClick(_ spaceID: UInt64, clickCount: Int) { if clickCount >= 2 { if pendingLockedSpaceUnlockSpaceID == spaceID { cancelPendingLockedSpaceUnlock() activateLockedSpace(spaceID) } else { DevelopmentDiagnostics.log("quickSwitch.spaceFilter.doubleClick.ignored", [ "spaceID": spaceID, "lockedSpaceID": lockedSpaceFilterID ?? NSNull(), "pendingUnlockSpaceID": pendingLockedSpaceUnlockSpaceID ?? NSNull() ]) } return } cancelPendingLockedSpaceUnlock() if lockedSpaceFilterID == spaceID { scheduleLockedSpaceUnlock(spaceID) return } applySpaceLaneSingleClick(spaceID) } private func applySpaceLaneSingleClick(_ spaceID: UInt64) { guard let sourceViewModel else { lastSpaceFilterAction = "blockedNoSourceViewModel" DevelopmentDiagnostics.log("quickSwitch.spaceFilter.click.blocked", [ @@ -605,6 +655,129 @@ onSnapshotUpdated?() } @discardableResult private func handleBackgroundClick() -> Bool { guard lockedSpaceFilterID != nil else { return false } cancelPendingLockedSpaceUnlock() let unlocked = unlockSpaceFilter(action: "unlockBackground") DevelopmentDiagnostics.log("quickSwitch.spaceFilter.backgroundUnlock", [ "unlocked": unlocked ]) return unlocked } private func scheduleLockedSpaceUnlock(_ spaceID: UInt64) { pendingLockedSpaceUnlockSpaceID = spaceID let delay = max(0.12, NSEvent.doubleClickInterval + 0.03) DevelopmentDiagnostics.log("quickSwitch.spaceFilter.unlock.pending", [ "spaceID": spaceID, "delay": delay ]) pendingLockedSpaceUnlockTask = Task { @MainActor [weak self] in try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) guard !Task.isCancelled, let self, self.pendingLockedSpaceUnlockSpaceID == spaceID else { return } self.pendingLockedSpaceUnlockSpaceID = nil self.pendingLockedSpaceUnlockTask = nil self.applySpaceLaneSingleClick(spaceID) } } private func cancelPendingLockedSpaceUnlock() { pendingLockedSpaceUnlockTask?.cancel() pendingLockedSpaceUnlockTask = nil pendingLockedSpaceUnlockSpaceID = nil } @discardableResult private func unlockSpaceFilter(action: String) -> Bool { guard let sourceViewModel, lockedSpaceFilterID != nil else { return false } lockedSpaceFilterID = nil let preferredSelection = selectionBeforeSpaceFilter selectionBeforeSpaceFilter = nil lastSpaceFilterAction = action let visibleViewModel = projectedViewModel( from: sourceViewModel, preferredSelection: preferredSelection ) currentViewModel = visibleViewModel view.applyProjected(viewModel: visibleViewModel) if disableScreenshotRefresh { view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption") screenshotTask?.cancel() screenshotTask = nil } else { scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration) } onSnapshotUpdated?() return true } private func activateLockedSpace(_ spaceID: UInt64) { guard lockedSpaceFilterID == spaceID else { DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.ignored", [ "spaceID": spaceID, "lockedSpaceID": lockedSpaceFilterID ?? NSNull() ]) return } guard let sourceViewModel else { lastSpaceFilterAction = "blockedNoSourceViewModel" DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.blocked", [ "spaceID": spaceID, "reason": "noSourceViewModel" ]) onSnapshotUpdated?() return } lockedSpaceFilterID = nil selectionBeforeSpaceFilter = nil lastSpaceFilterAction = "activateSpace" lastSpaceActivationSpaceID = spaceID lastSpaceActivationDidRequestFocus = nil lastSpaceActivationDisplayIdentifier = nil lastSpaceActivationPreviousCurrentSpaceID = nil lastSpaceActivationError = nil let visibleViewModel = projectedViewModel(from: sourceViewModel) currentViewModel = visibleViewModel view.applyProjected(viewModel: visibleViewModel) DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.start", [ "spaceID": spaceID ]) hide(reason: .userClosed) let outcome = spaceActivationService.activate(spaceID: spaceID) lastSpaceActivationSpaceID = outcome.targetSpaceID lastSpaceActivationDidRequestFocus = outcome.didRequestFocus lastSpaceActivationDisplayIdentifier = outcome.displayIdentifier lastSpaceActivationPreviousCurrentSpaceID = outcome.previousCurrentSpaceID lastSpaceActivationError = outcome.error DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.result", [ "spaceID": outcome.targetSpaceID, "displayIdentifier": outcome.displayIdentifier ?? NSNull(), "previousCurrentSpaceID": outcome.previousCurrentSpaceID ?? NSNull(), "didRequestFocus": outcome.didRequestFocus, "error": outcome.error ?? NSNull() ]) onSnapshotUpdated?() } private func requestClose(_ request: QuickSwitchCloseRequest) { DevelopmentDiagnostics.log("quickSwitch.close.request.start", [ "targetKind": request.kindDescription, C1.source/Sources/Aligner/SpaceActivationService.swift
New file @@ -0,0 +1,39 @@ import Foundation protocol SpaceActivationServiceProtocol: AnyObject { func activate(spaceID: UInt64) -> PrivateSpaceFocusOutcome } final class PrivateSpaceActivationService: SpaceActivationServiceProtocol { private let bridge: PrivateSpaceActivationBridge? init(bridge: PrivateSpaceActivationBridge? = .shared) { self.bridge = bridge } func activate(spaceID: UInt64) -> PrivateSpaceFocusOutcome { guard let outcome = bridge?.focus(spaceIDs: [spaceID]) else { return PrivateSpaceFocusOutcome( targetSpaceID: spaceID, displayIdentifier: nil, previousCurrentSpaceID: nil, didRequestFocus: false, error: "spaceActivationBridgeUnavailable" ) } return outcome } } final class DebugSpaceActivationService: SpaceActivationServiceProtocol { func activate(spaceID: UInt64) -> PrivateSpaceFocusOutcome { PrivateSpaceFocusOutcome( targetSpaceID: spaceID, displayIdentifier: "debug", previousCurrentSpaceID: nil, didRequestFocus: true, error: nil ) } } C3.tools/round1-space-filter-fixture-qa.sh
@@ -69,8 +69,9 @@ wait_for_visible_report() { local report="$1" local expected_last_mouse="$2" local expected_action="${3:-}" /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" <<'PY' /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" "$expected_action" <<'PY' import json import sys import time @@ -78,6 +79,7 @@ path = sys.argv[1] timeout = float(sys.argv[2]) expected_last_mouse = sys.argv[3] expected_action = sys.argv[4] deadline = time.monotonic() + timeout last_report = None @@ -91,6 +93,7 @@ report.get("snapshotLoaded") is True and report.get("quickSwitchVisible") is True and root.get("lastMouseCommand") == expected_last_mouse and (expected_action == "" or report.get("lastSpaceFilterAction") == expected_action) ): sys.exit(0) except FileNotFoundError: @@ -101,7 +104,11 @@ if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print(f"visible report did not reach mouse command {expected_last_mouse!r} within {timeout:.1f}s", file=sys.stderr) print( f"visible report did not reach mouse command {expected_last_mouse!r}" f" and action {expected_action!r} within {timeout:.1f}s", file=sys.stderr ) sys.exit(1) PY } @@ -148,11 +155,54 @@ PY } wait_for_space_activation_report() { local report="$1" local expected_last_mouse="$2" /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" <<'PY' import json import sys import time path = sys.argv[1] timeout = float(sys.argv[2]) expected_last_mouse = sys.argv[3] deadline = time.monotonic() + timeout last_report = None while time.monotonic() < deadline: try: with open(path, "r", encoding="utf-8") as file: report = json.load(file) last_report = report root = report.get("rootView", {}) if ( report.get("snapshotLoaded") is True and report.get("quickSwitchVisible") is False and report.get("lastSpaceFilterAction") == "activateSpace" and report.get("lastSpaceActivationSpaceID") is not None and root.get("lastMouseCommand") == expected_last_mouse ): sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.2) if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print(f"space activation report did not reach mouse command {expected_last_mouse!r} within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } run_report() { local report="$1" local sequence="$2" local expected_last_mouse="$3" local quick_switch_expectation="${4:-visible}" local expected_action="${5:-}" rm -f "$report" "$APP/Contents/MacOS/Aligner" \ @@ -169,8 +219,11 @@ if [ "$quick_switch_expectation" = "hidden" ]; then wait_for_activation_report "$report" "$expected_last_mouse" swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2 elif [ "$quick_switch_expectation" = "space-hidden" ]; then wait_for_space_activation_report "$report" "$expected_last_mouse" swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2 elif [ "$quick_switch_expectation" = "visible" ]; then wait_for_visible_report "$report" "$expected_last_mouse" wait_for_visible_report "$report" "$expected_last_mouse" "$expected_action" swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2 else fail "unknown quick switch expectation: $quick_switch_expectation" @@ -228,6 +281,11 @@ require(segment_by_id[4].get("windowCount") == 0, "B1 must remain visible as empty Space") require(segment_by_id[5].get("windowCount") == 1, "B2 fullscreen Space must remain visible") require("locked" in segment_by_id[1].get("visualStates", []), "locked Space must expose locked visual state") require(segment_by_id[1].get("borderWidth", 0) >= 2.5, "locked Space must use a stronger blue border") lock_shadow = segment_by_id[1].get("shadowColor", {}) require(lock_shadow.get("red", 0) >= 0.90 and lock_shadow.get("green", 0) >= 0.90 and lock_shadow.get("blue", 0) >= 0.90, "locked Space must use a white glow") require(segment_by_id[1].get("shadowOpacity", 0) >= 0.60, "locked Space glow must be visibly stronger than hover") require(segment_by_id[1].get("shadowRadius", 0) >= 26, "locked Space glow radius must be strong enough") require("locked" not in segment_by_id[5].get("visualStates", []), "single fullscreen Space must not be marked locked") require(root.get("projectionTransitionFadeOutLayerCount", 0) >= 1, "filter transition must fade out removed visual layers") require(1 <= root.get("projectionTransitionDurationMilliseconds", 0) <= 220, "filter transition duration must stay within P0 native-feel budget") @@ -238,6 +296,9 @@ "columns": root.get("waterfallColumnNames"), "cardWindowIDs": [card.get("windowID") for card in cards], "lockedSpace": root.get("spaceFilterLockedSpaceID"), "lockedBorderWidth": segment_by_id[1].get("borderWidth"), "lockedShadowOpacity": segment_by_id[1].get("shadowOpacity"), "lockedShadowRadius": segment_by_id[1].get("shadowRadius"), "fadeOutLayerCount": root.get("projectionTransitionFadeOutLayerCount"), "transitionDurationMilliseconds": root.get("projectionTransitionDurationMilliseconds") }, indent=2, ensure_ascii=False)) @@ -316,6 +377,41 @@ PY } assert_background_unlock_report() { /usr/bin/python3 - "$1" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as file: report = json.load(file) def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(1) root = report.get("rootView", {}) cards = [card for column in root.get("waterfallColumns", []) for card in column.get("cards", [])] require(report.get("spaceFilterActive") is False, "background click must unlock session filter") require(root.get("spaceFilterActive") is False, "background click must unlock root filter") require(report.get("lastSpaceFilterAction") == "unlockBackground", "background click must record unlockBackground") require(root.get("lastMouseCommand") == "click-background", "last mouse command must be click-background") require(report.get("appShelfNames") == ["Alpha Space App", "Beta Space App", "Fullscreen Solo App", "No Space App"], "background unlock must restore all fixture apps") require(report.get("windowCount") == 6, "background unlock must restore all six fixture windows") require(50901 in [card.get("windowID") for card in cards], "no-space window should be visible again after background unlock") print(json.dumps({ "case": "background-unlock", "apps": report.get("appShelfNames"), "windowCount": report.get("windowCount"), "lastSpaceFilterAction": report.get("lastSpaceFilterAction") }, indent=2, ensure_ascii=False)) PY } assert_empty_a3_report() { /usr/bin/python3 - "$1" <<'PY' import json @@ -345,6 +441,10 @@ require(root.get("waterfallColumnCount") == 0, "root Waterfall must be empty") require(root.get("spaceLaneLabels") == ["A1", "A2", "A3", "B1", "B2"], "Space Lane must stay global while empty Space is locked") require("locked" in segment_by_id[3].get("visualStates", []), "empty locked Space must expose locked visual state") require(segment_by_id[3].get("borderWidth", 0) >= 2.5, "empty locked Space must still use strong locked border") empty_shadow = segment_by_id[3].get("shadowColor", {}) require(empty_shadow.get("red", 0) >= 0.90 and empty_shadow.get("green", 0) >= 0.90 and empty_shadow.get("blue", 0) >= 0.90, "empty locked Space must use white glow") require(segment_by_id[3].get("shadowOpacity", 0) >= 0.60, "empty locked Space glow must be visible") require(segment_by_id[3].get("backgroundKind") == "clear", "empty locked Space must not use gray/gradient fill") require(segment_by_id[4].get("backgroundKind") == "clear", "empty sibling Space must not inherit gray/gradient fill") @@ -355,6 +455,47 @@ "columnCount": report.get("columnCount"), "a3BackgroundKind": segment_by_id[3].get("backgroundKind"), "b1BackgroundKind": segment_by_id[4].get("backgroundKind") }, indent=2, ensure_ascii=False)) PY } assert_locked_space_double_click_report() { /usr/bin/python3 - "$1" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as file: report = json.load(file) def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(1) root = report.get("rootView", {}) require(report.get("quickSwitchVisible") is False, "double-click locked Space must close Quick Switch") require(report.get("spaceFilterActive") is False, "double-click locked Space must release filter") require(root.get("spaceFilterActive") is False, "root must show no filter after Space activation") require(report.get("lastSpaceFilterAction") == "activateSpace", "double-click locked Space must record activateSpace") require(report.get("lastCommittedWindowID") is None, "double-click Space must not commit a window") require(report.get("lastActivationWindowID") is None, "double-click Space must not target a window") require(report.get("lastActivationResult") is None, "double-click Space must not run window activation") require(report.get("lastCommitSource") is None, "double-click Space must not have a commit source") require(report.get("lastSpaceActivationSpaceID") == 1, "double-click locked A1 must request Space 1 activation") require(report.get("lastSpaceActivationDidRequestFocus") is True, "debug Space activation must request focus") require(report.get("lastSpaceActivationDisplayIdentifier") == "debug", "fixture must use debug Space activation service") require(report.get("lastSpaceActivationError") is None, "debug Space activation must not fail") require(root.get("lastMouseCommand") == "double-click-space:1", "last mouse command must be double-click-space:1") print(json.dumps({ "case": "locked-space-double-click", "quickSwitchVisible": report.get("quickSwitchVisible"), "lastSpaceFilterAction": report.get("lastSpaceFilterAction"), "lastSpaceActivationSpaceID": report.get("lastSpaceActivationSpaceID"), "lastSpaceActivationDisplayIdentifier": report.get("lastSpaceActivationDisplayIdentifier") }, indent=2, ensure_ascii=False)) PY } @@ -408,20 +549,25 @@ LOCK_A1_REPORT="$REPORT_PREFIX-lock-a1-report.json" SWITCH_A2_REPORT="$REPORT_PREFIX-switch-a2-report.json" UNLOCK_REPORT="$REPORT_PREFIX-unlock-report.json" BACKGROUND_UNLOCK_REPORT="$REPORT_PREFIX-background-unlock-report.json" EMPTY_A3_REPORT="$REPORT_PREFIX-empty-a3-report.json" FULLSCREEN_REPORT="$REPORT_PREFIX-fullscreen-report.json" LOCK_THEN_FULLSCREEN_REPORT="$REPORT_PREFIX-lock-then-fullscreen-report.json" LOCKED_DOUBLE_CLICK_REPORT="$REPORT_PREFIX-locked-double-click-report.json" run_report "$LOCK_A1_REPORT" "click-space:1" "click-space:1" "visible" run_report "$LOCK_A1_REPORT" "click-space:1" "click-space:1" "visible" "lock" assert_lock_a1_report "$LOCK_A1_REPORT" run_report "$SWITCH_A2_REPORT" "click-space:1,click-space:2" "click-space:2" "visible" run_report "$SWITCH_A2_REPORT" "click-space:1,click-space:2" "click-space:2" "visible" "switch" assert_switch_a2_report "$SWITCH_A2_REPORT" run_report "$UNLOCK_REPORT" "click-space:1,click-space:1" "click-space:1" "visible" run_report "$UNLOCK_REPORT" "click-space:1,click-space:1" "click-space:1" "visible" "unlock" assert_unlock_report "$UNLOCK_REPORT" run_report "$EMPTY_A3_REPORT" "click-space:3" "click-space:3" "visible" run_report "$BACKGROUND_UNLOCK_REPORT" "click-space:1,click-background" "click-background" "visible" "unlockBackground" assert_background_unlock_report "$BACKGROUND_UNLOCK_REPORT" run_report "$EMPTY_A3_REPORT" "click-space:3" "click-space:3" "visible" "lock" assert_empty_a3_report "$EMPTY_A3_REPORT" run_report "$FULLSCREEN_REPORT" "click-space:5" "click-space:5" "hidden" @@ -430,6 +576,9 @@ run_report "$LOCK_THEN_FULLSCREEN_REPORT" "click-space:1,click-space:5" "click-space:5" "hidden" assert_single_fullscreen_report "$LOCK_THEN_FULLSCREEN_REPORT" "lock-then-single-fullscreen" run_report "$LOCKED_DOUBLE_CLICK_REPORT" "click-space:1,double-click-space:1" "double-click-space:1" "space-hidden" assert_locked_space_double_click_report "$LOCKED_DOUBLE_CLICK_REPORT" trap - EXIT cleanup