| 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/QuickSwitchSessionController.swift | ●●●●● patch | view | raw | blame | history | |
| C3.tools/round1-view-mode-entry-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.57</string> <string>0.0.58</string> <key>CFBundleVersion</key> <string>20260612.1151</string> <string>20260612.1249</string> <key>LSMinimumSystemVersion</key> <string>26.0</string> <key>NSHighResolutionCapable</key> C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -3,10 +3,11 @@ import AlignerCore @MainActor final class AlignerApplicationDelegate: NSObject, NSApplicationDelegate { final class AlignerApplicationDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { private static let quickSwitchConfirmBeforeCloseDefaultsKey = "QuickSwitchConfirmBeforeClose" private let permissionChecker = SystemPermissionChecker() private let preferenceStore = UserDefaultsPreferenceStore() private let launchOptions = Round0DebugLaunchOptions.parse(arguments: ProcessInfo.processInfo.arguments) private let round1PoCOptions = Round1PerformancePoCOptions.parse(arguments: ProcessInfo.processInfo.arguments) private let round1TriggerProbeOptions = Round1TriggerProbeOptions.parse(arguments: ProcessInfo.processInfo.arguments) @@ -21,6 +22,8 @@ private var quickSwitchTriggerService: CGEventTapTriggerService? private var isRunningQuickSwitchLifecycle = false private var statusItem: NSStatusItem? private var verticalWaterfallViewModeMenuItem: NSMenuItem? private var horizontalWaterfallViewModeMenuItem: NSMenuItem? func applicationDidFinishLaunching(_ notification: Notification) { let bundlePathSummary = DevelopmentDiagnostics.pathSummary(Bundle.main.bundlePath) @@ -91,21 +94,68 @@ item.button?.title = "A" let menu = NSMenu() menu.addItem(NSMenuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ",")) menu.delegate = self menu.addItem(menuItem(title: "About Aligner...", action: #selector(showAbout), keyEquivalent: "")) menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: "")) menu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ",")) menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Show Round0 Debug Overlay", action: #selector(showRound0DebugOverlay), keyEquivalent: "d")) menu.addItem(NSMenuItem(title: "Hide Round0 Debug Overlay", action: #selector(hideRound0DebugOverlay), keyEquivalent: "")) menu.addItem(menuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: "")) menu.addItem(menuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: "")) menu.addItem(quickSwitchViewModeMenuItem()) menu.addItem(.separator()) menu.addItem(NSMenuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q")) menu.addItem(menuItem(title: "Show Round0 Debug Overlay", action: #selector(showRound0DebugOverlay), keyEquivalent: "d")) menu.addItem(menuItem(title: "Hide Round0 Debug Overlay", action: #selector(hideRound0DebugOverlay), keyEquivalent: "")) menu.addItem(.separator()) menu.addItem(menuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q")) item.menu = menu statusItem = item updateWaterfallViewModeMenuState() DevelopmentDiagnostics.log("app.statusItem.configure.end", [ "hasButton": item.button != nil "hasButton": item.button != nil, "hasAboutItem": true, "hasWaterfallViewModeItems": verticalWaterfallViewModeMenuItem != nil && horizontalWaterfallViewModeMenuItem != nil ]) } func menuWillOpen(_ menu: NSMenu) { updateWaterfallViewModeMenuState() } private func menuItem(title: String, action: Selector?, keyEquivalent: String) -> NSMenuItem { let item = NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent) item.target = self return item } private func quickSwitchViewModeMenuItem() -> NSMenuItem { let submenu = NSMenu(title: "Quick Switch View") let verticalItem = menuItem( title: "Vertical Columns", action: #selector(useVerticalWaterfallViewMode), keyEquivalent: "" ) let horizontalItem = menuItem( title: "Horizontal Waterfall", action: #selector(useHorizontalWaterfallViewMode), keyEquivalent: "" ) submenu.addItem(verticalItem) submenu.addItem(horizontalItem) verticalWaterfallViewModeMenuItem = verticalItem horizontalWaterfallViewModeMenuItem = horizontalItem let rootItem = NSMenuItem(title: "Quick Switch View", action: nil, keyEquivalent: "") rootItem.submenu = submenu return rootItem } private func updateWaterfallViewModeMenuState() { let mode = quickSwitchWaterfallViewMode() verticalWaterfallViewModeMenuItem?.state = mode == .verticalColumns ? .on : .off horizontalWaterfallViewModeMenuItem?.state = mode == .horizontalMasonry ? .on : .off } private func showPermissionsIfNeeded() { @@ -468,13 +518,19 @@ guard !isRunningQuickSwitchLifecycle else { return } let reportPathSummary = DevelopmentDiagnostics.pathSummary(reportPath) let report = quickSwitchSessionController?.reportDictionary() ?? [ var report: [String: Any] = quickSwitchSessionController?.reportDictionary() ?? [ "snapshotLoaded": false, "quickSwitchVisible": false, "permissionStatus": "\(accessibilityStatus())", "diagnosticLogLocationKind": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["locationKind"] ?? "unknown", "diagnosticLogBasename": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["basename"] ?? "aligner-dev.log" ] let versionBuild = appVersionBuild() report["preferenceWaterfallViewMode"] = preferenceStore.read().quickSwitchWaterfallViewMode.rawValue report["resolvedWaterfallViewMode"] = quickSwitchWaterfallViewMode().rawValue report["aboutVersion"] = versionBuild.version report["aboutBuild"] = versionBuild.build report["statusMenu"] = statusMenuReportDictionary() do { let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys]) @@ -552,7 +608,7 @@ private func quickSwitchWaterfallViewMode() -> QuickSwitchWaterfallViewMode { round1QuickSwitchOptions.waterfallViewMode ?? UserDefaultsPreferenceStore().read().quickSwitchWaterfallViewMode ?? preferenceStore.read().quickSwitchWaterfallViewMode } private func quickSwitchSpaceActivationService() -> any SpaceActivationServiceProtocol { @@ -583,6 +639,71 @@ ]) } private func setQuickSwitchWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) { let preferences = preferenceStore.read() let updatedPreferences = AlignerPreferences( showMinimizedWindows: preferences.showMinimizedWindows, showFullscreenWindows: preferences.showFullscreenWindows, quickSwitchWaterfallViewMode: mode, theme: preferences.theme, language: preferences.language, spaceDisplayStrategy: preferences.spaceDisplayStrategy ) preferenceStore.write(updatedPreferences) quickSwitchSessionController?.setWaterfallViewMode(mode) updateWaterfallViewModeMenuState() writeQuickSwitchReportIfNeeded() DevelopmentDiagnostics.log("app.quickSwitch.waterfallViewModePreferenceChanged", [ "mode": mode.rawValue, "quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false ]) } private func appVersionBuild() -> (version: String, build: String) { let info = Bundle.main.infoDictionary ?? [:] let version = info["CFBundleShortVersionString"] as? String ?? "Unknown" let build = info["CFBundleVersion"] as? String ?? "Unknown" return (version, build) } private func statusMenuReportDictionary() -> [String: Any] { updateWaterfallViewModeMenuState() return [ "hasAboutItem": statusItem?.menu?.item(withTitle: "About Aligner...") != nil, "hasQuickSwitchViewSubmenu": statusItem?.menu?.item(withTitle: "Quick Switch View")?.submenu != nil, "hasVerticalColumnsItem": verticalWaterfallViewModeMenuItem != nil, "hasHorizontalWaterfallItem": horizontalWaterfallViewModeMenuItem != nil, "verticalColumnsState": verticalWaterfallViewModeMenuItem?.state == .on ? "on" : "off", "horizontalWaterfallState": horizontalWaterfallViewModeMenuItem?.state == .on ? "on" : "off" ] } @objc private func showAbout() { let versionBuild = appVersionBuild() DevelopmentDiagnostics.log("app.menu.showAbout", [ "version": versionBuild.version, "build": versionBuild.build ]) NSApp.setActivationPolicy(.regular) let alert = NSAlert() alert.messageText = "Aligner" alert.informativeText = "Version / Build: \(versionBuild.version) / \(versionBuild.build)" alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.icon = NSApp.applicationIconImage NSApp.activate(ignoringOtherApps: true) alert.runModal() } @objc private func useVerticalWaterfallViewMode() { setQuickSwitchWaterfallViewMode(.verticalColumns) } @objc private func useHorizontalWaterfallViewMode() { setQuickSwitchWaterfallViewMode(.horizontalMasonry) } @objc private func showPermissions() { DevelopmentDiagnostics.log("app.permissions.show", [ "accessibility": permissionStatusString(accessibilityStatus()), C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -19,7 +19,7 @@ private let windowCloseService: any WindowCloseServiceProtocol private let systemCriticalWindowDetector: any SystemCriticalWindowDetecting private let disableScreenshotRefresh: Bool private let waterfallViewMode: QuickSwitchWaterfallViewMode private var waterfallViewMode: QuickSwitchWaterfallViewMode private var closeConfirmationRequired: Bool private let onCloseConfirmationDisabled: (() -> Void)? private let debugOverlayWidth: CGFloat? @@ -279,6 +279,23 @@ show() } func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) { guard waterfallViewMode != mode else { return } waterfallViewMode = mode DevelopmentDiagnostics.log("quickSwitch.session.waterfallViewModeChanged", [ "mode": mode.rawValue, "visible": isVisible, "hasViewModel": currentViewModel != nil ]) view.setWaterfallViewMode(mode) if isVisible, let currentViewModel { view.applyProjected(viewModel: currentViewModel) } onSnapshotUpdated?() } func reportDictionary() -> [String: Any] { let spaceCount = currentViewModel?.spaceCount ?? 0 let appCount = currentViewModel?.appShelf.count ?? 0 C3.tools/round1-view-mode-entry-qa.sh
New file @@ -0,0 +1,202 @@ #!/bin/bash # Round01.1 view-mode entry QA. It verifies that the installed-app path exposes # a discoverable status-menu entry for switching Quick Switch Waterfall view # modes and reports visible About version/build metadata. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=build-output-paths.sh source "$SCRIPT_DIR/build-output-paths.sh" APP="$BUILD_CURRENT_APP" REPORT_VERTICAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-vertical-report.json" REPORT_HORIZONTAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-horizontal-report.json" DOMAIN="com.ar.Aligner" WATERFALL_MODE_KEY="com.ar.Aligner.preferences.quickSwitch.waterfallViewMode" REPORT_WAIT="${ALIGNER_ROUND1_VIEW_MODE_ENTRY_REPORT_WAIT:-6.0}" OLD_WATERFALL_MODE_SET=0 OLD_WATERFALL_MODE="" fail() { echo "Round01.1 view-mode entry QA failed: $*" >&2 exit 1 } aligner_pids_for_current_app() { ps -axo pid=,args= | while read -r pid command; do case "$command" in "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;; esac done } stop_current_aligner() { for pid in $(aligner_pids_for_current_app); do kill "$pid" 2>/dev/null || true done for _ in {1..30}; do [ -z "$(aligner_pids_for_current_app)" ] && return sleep 0.1 done fail "current Aligner app did not exit before QA" } preserve_user_preference() { if OLD_WATERFALL_MODE="$(/usr/bin/defaults read "$DOMAIN" "$WATERFALL_MODE_KEY" 2>/dev/null)"; then OLD_WATERFALL_MODE_SET=1 else OLD_WATERFALL_MODE_SET=0 OLD_WATERFALL_MODE="" fi } restore_user_preference() { if [ "$OLD_WATERFALL_MODE_SET" -eq 1 ]; then /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "$OLD_WATERFALL_MODE" else /usr/bin/defaults delete "$DOMAIN" "$WATERFALL_MODE_KEY" >/dev/null 2>&1 || true fi } wait_for_loaded_report() { local report="$1" local expected_mode="$2" /usr/bin/python3 - "$report" "$expected_mode" "$REPORT_WAIT" <<'PY' import json import sys import time path = sys.argv[1] expected_mode = sys.argv[2] timeout = float(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("resolvedWaterfallViewMode") == expected_mode and root.get("waterfallViewMode") == expected_mode ): 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"view-mode entry report did not reach {expected_mode} within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } assert_report() { local report="$1" local expected_mode="$2" local expected_vertical_state="$3" local expected_horizontal_state="$4" local app_version="$5" local app_build="$6" /usr/bin/python3 - "$report" "$expected_mode" "$expected_vertical_state" "$expected_horizontal_state" "$app_version" "$app_build" <<'PY' import json import re import sys path = sys.argv[1] expected_mode = sys.argv[2] expected_vertical_state = sys.argv[3] expected_horizontal_state = sys.argv[4] app_version = sys.argv[5] app_build = sys.argv[6] 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) status_menu = report.get("statusMenu", {}) root = report.get("rootView", {}) require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(report.get("quickSwitchVisible") is True, "Quick Switch must be visible during entry QA") require(report.get("preferenceWaterfallViewMode") == expected_mode, "preference Waterfall mode must match expected mode") require(report.get("resolvedWaterfallViewMode") == expected_mode, "resolved Waterfall mode must match expected mode") require(root.get("waterfallViewMode") == expected_mode, "root view Waterfall mode must match expected mode") require(status_menu.get("hasAboutItem") is True, "status menu must expose About Aligner") require(status_menu.get("hasQuickSwitchViewSubmenu") is True, "status menu must expose Quick Switch View submenu") require(status_menu.get("hasVerticalColumnsItem") is True, "status menu must expose Vertical Columns item") require(status_menu.get("hasHorizontalWaterfallItem") is True, "status menu must expose Horizontal Waterfall item") require(status_menu.get("verticalColumnsState") == expected_vertical_state, "Vertical Columns check state must match expected mode") require(status_menu.get("horizontalWaterfallState") == expected_horizontal_state, "Horizontal Waterfall check state must match expected mode") require(report.get("aboutVersion") == app_version, "About version must match app Info.plist") require(report.get("aboutBuild") == app_build, "About build must match app Info.plist") require(re.match(r"^[0-9]+\.[0-9]+\.[0-9]+$", app_version) is not None, "version must be MAJOR.MINOR.PATCH") require(re.match(r"^[0-9]{8}\.[0-9]{4}$", app_build) is not None, "build must be YYYYMMDD.HHMM") print(json.dumps({ "mode": expected_mode, "aboutVersion": report.get("aboutVersion"), "aboutBuild": report.get("aboutBuild"), "statusMenu": status_menu }, indent=2, ensure_ascii=False)) PY } run_case() { local mode="$1" local report="$2" local vertical_state="$3" local horizontal_state="$4" local app_version="$5" local app_build="$6" stop_current_aligner rm -f "$report" /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "$mode" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-fixture-app-count=4 \ --round01-fixture-windows-per-app=2 \ --round01-disable-screenshot-refresh \ --round01-quick-switch-report="$report" \ --round01-quick-switch-auto-hide-after=1.0 \ --round01-quick-switch-quit-after=1.2 & wait_for_loaded_report "$report" "$mode" assert_report "$report" "$mode" "$vertical_state" "$horizontal_state" "$app_version" "$app_build" wait "$!" || true stop_current_aligner } [ -x "$APP/Contents/MacOS/Aligner" ] || fail "missing executable app at $APP" INFO_PLIST="$APP/Contents/Info.plist" APP_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$INFO_PLIST" 2>/dev/null)" \ || fail "unable to read CFBundleShortVersionString from $INFO_PLIST" APP_BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$INFO_PLIST" 2>/dev/null)" \ || fail "unable to read CFBundleVersion from $INFO_PLIST" preserve_user_preference trap 'stop_current_aligner; restore_user_preference' EXIT run_case "verticalColumns" "$REPORT_VERTICAL" "on" "off" "$APP_VERSION" "$APP_BUILD" run_case "horizontalMasonry" "$REPORT_HORIZONTAL" "off" "on" "$APP_VERSION" "$APP_BUILD" echo "Round01.1 view-mode entry QA passed"