import AppKit
|
import Darwin
|
import AlignerCore
|
|
@MainActor
|
final class AlignerApplicationDelegate: NSObject, NSApplicationDelegate {
|
private static let quickSwitchConfirmBeforeCloseDefaultsKey = "QuickSwitchConfirmBeforeClose"
|
|
private let permissionChecker = SystemPermissionChecker()
|
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)
|
private let round1SnapshotDumpOptions = Round1SnapshotDumpOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1ScreenshotProbeOptions = Round1ScreenshotProbeOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1QuickSwitchOptions = Round1QuickSwitchLaunchOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private var permissionWindowController: PermissionsWindowController?
|
private var debugOverlayController: Round0DebugOverlayController?
|
private var round1PerformancePoCController: Round1PerformancePoCController?
|
private var round1TriggerProbe: CGEventTapTriggerProbe?
|
private var quickSwitchSessionController: QuickSwitchSessionController?
|
private var quickSwitchTriggerService: CGEventTapTriggerService?
|
private var isRunningQuickSwitchLifecycle = false
|
private var statusItem: NSStatusItem?
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
let bundlePathSummary = DevelopmentDiagnostics.pathSummary(Bundle.main.bundlePath)
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.start", [
|
"bundlePathKind": bundlePathSummary["locationKind"],
|
"bundlePathBasename": bundlePathSummary["basename"],
|
"bundlePathHash": bundlePathSummary["fingerprint"],
|
"accessibility": permissionStatusString(accessibilityStatus()),
|
"screenRecording": permissionStatusString(screenRecordingStatus()),
|
"skipPermissions": launchOptions.skipPermissions,
|
"openQuickSwitch": round1QuickSwitchOptions.openQuickSwitch,
|
"quickSwitchLifecycle": round1QuickSwitchOptions.lifecycleCycles
|
])
|
|
if runRound1SnapshotDumpIfRequested() {
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.exitForSnapshotDump")
|
return
|
}
|
if runRound1ScreenshotProbeIfRequested() {
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.exitForScreenshotProbe")
|
return
|
}
|
|
configureStatusItem()
|
if !launchOptions.skipPermissions {
|
showPermissionsIfNeeded()
|
}
|
startQuickSwitchTriggerServiceIfPossible()
|
showDebugOverlayIfRequested()
|
showRound1PerformancePoCIfRequested()
|
showQuickSwitchIfRequested()
|
startRound1TriggerProbeIfRequested()
|
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.end", [
|
"activationPolicy": NSApp.activationPolicy().rawValue,
|
"quickSwitchTriggerRunning": quickSwitchTriggerService?.isRunning ?? false,
|
"statusItemVisible": statusItem != nil
|
])
|
}
|
|
func applicationWillTerminate(_ notification: Notification) {
|
DevelopmentDiagnostics.logSync("app.lifecycle.willTerminate", [
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false,
|
"hasPermissionWindow": permissionWindowController?.window != nil,
|
"activationPolicy": NSApp.activationPolicy().rawValue
|
])
|
}
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
DevelopmentDiagnostics.log("app.lifecycle.shouldTerminateAfterLastWindowClosed", [
|
"decision": false,
|
"visibleWindowCount": sender.windows.filter(\.isVisible).count
|
])
|
return false
|
}
|
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
DevelopmentDiagnostics.log("app.lifecycle.shouldHandleReopen", [
|
"hasVisibleWindows": flag,
|
"activationPolicy": sender.activationPolicy().rawValue
|
])
|
return true
|
}
|
|
private func configureStatusItem() {
|
DevelopmentDiagnostics.log("app.statusItem.configure.start")
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
item.button?.title = "A"
|
|
let menu = NSMenu()
|
menu.addItem(NSMenuItem(title: "Check Permissions...", action: #selector(showPermissions), 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(.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(.separator())
|
menu.addItem(NSMenuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q"))
|
item.menu = menu
|
|
statusItem = item
|
DevelopmentDiagnostics.log("app.statusItem.configure.end", [
|
"hasButton": item.button != nil
|
])
|
}
|
|
private func showPermissionsIfNeeded() {
|
let accessibility = accessibilityStatus()
|
let screenRecording = screenRecordingStatus()
|
DevelopmentDiagnostics.log("app.permissions.checkOnLaunch", [
|
"accessibility": permissionStatusString(accessibility),
|
"screenRecording": permissionStatusString(screenRecording)
|
])
|
if accessibility != .granted || screenRecording != .granted {
|
showPermissions()
|
}
|
}
|
|
private func runRound1SnapshotDumpIfRequested() -> Bool {
|
guard round1SnapshotDumpOptions.enabled else { return false }
|
DevelopmentDiagnostics.log("app.diagnostic.snapshotDump.start")
|
|
do {
|
try Round1SnapshotDump.run(prettyPrinted: round1SnapshotDumpOptions.prettyPrinted)
|
DevelopmentDiagnostics.logSync("app.diagnostic.snapshotDump.success")
|
NSApp.terminate(nil)
|
} catch {
|
DevelopmentDiagnostics.logSync("app.diagnostic.snapshotDump.failed", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 snapshot dump failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
exit(2)
|
}
|
|
return true
|
}
|
|
private func runRound1ScreenshotProbeIfRequested() -> Bool {
|
guard round1ScreenshotProbeOptions.enabled else { return false }
|
DevelopmentDiagnostics.log("app.diagnostic.screenshotProbe.start")
|
|
Task { @MainActor in
|
do {
|
try await Round1ScreenshotProbe.run(prettyPrinted: round1ScreenshotProbeOptions.prettyPrinted)
|
DevelopmentDiagnostics.logSync("app.diagnostic.screenshotProbe.success")
|
NSApp.terminate(nil)
|
} catch {
|
DevelopmentDiagnostics.logSync("app.diagnostic.screenshotProbe.failed", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 screenshot probe failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
exit(2)
|
}
|
}
|
|
return true
|
}
|
|
private func showDebugOverlayIfRequested() {
|
guard launchOptions.showOverlay else { return }
|
|
showDebugOverlay(includeSettingsChild: launchOptions.showSettingsChild)
|
|
if let autoHideAfter = launchOptions.autoHideAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + autoHideAfter) { [weak self] in
|
self?.hideDebugOverlay()
|
}
|
}
|
|
if let quitAfter = launchOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) {
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func showRound1PerformancePoCIfRequested() {
|
guard round1PoCOptions.showPerformancePoC else { return }
|
|
if let lifecycleCycles = round1PoCOptions.lifecycleCycles {
|
runRound1PerformancePoCLifecycle(cycles: lifecycleCycles)
|
return
|
}
|
|
openRound1PerformancePoC(autoCycle: round1PoCOptions.autoCycle)
|
|
if let autoHideAfter = round1PoCOptions.autoHideAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + autoHideAfter) { [weak self] in
|
self?.writeRound1PerformancePoCReportIfNeeded()
|
self?.hideRound1PerformancePoC()
|
}
|
}
|
|
if let quitAfter = round1PoCOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) { [weak self] in
|
self?.writeRound1PerformancePoCReportIfNeeded()
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func runRound1PerformancePoCLifecycle(cycles: Int) {
|
if round1PerformancePoCController == nil {
|
round1PerformancePoCController = Round1PerformancePoCController()
|
}
|
|
round1PerformancePoCController?.prepareLifecycleRun(targetCycles: cycles)
|
runRound1PerformancePoCLifecycleCycle(currentCycle: 0, totalCycles: max(0, cycles))
|
}
|
|
private func runRound1PerformancePoCLifecycleCycle(currentCycle: Int, totalCycles: Int) {
|
guard currentCycle < totalCycles else {
|
round1PerformancePoCController?.finalizeLifecycleRun()
|
writeRound1PerformancePoCReportIfNeeded()
|
NSApp.terminate(nil)
|
return
|
}
|
|
openRound1PerformancePoC(autoCycle: false)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + round1PoCOptions.lifecycleVisibleDuration) { [weak self] in
|
guard let self else { return }
|
self.hideRound1PerformancePoC()
|
self.round1PerformancePoCController?.markLifecycleCycleCompleted()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + self.round1PoCOptions.lifecycleInterval) { [weak self] in
|
self?.runRound1PerformancePoCLifecycleCycle(currentCycle: currentCycle + 1, totalCycles: totalCycles)
|
}
|
}
|
}
|
|
private func startRound1TriggerProbeIfRequested() {
|
guard round1TriggerProbeOptions.enabled else { return }
|
DevelopmentDiagnostics.log("app.triggerProbe.startRequested")
|
|
let probe = CGEventTapTriggerProbe()
|
|
do {
|
try probe.start()
|
round1TriggerProbe = probe
|
DevelopmentDiagnostics.log("app.triggerProbe.started")
|
print("Round01 trigger probe started")
|
fflush(stdout)
|
} catch {
|
DevelopmentDiagnostics.logSync("app.triggerProbe.failed", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 trigger probe failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
exit(2)
|
}
|
|
if let quitAfter = round1TriggerProbeOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) {
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func startQuickSwitchTriggerServiceIfPossible() {
|
guard !round1TriggerProbeOptions.enabled else {
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.skip", [
|
"reason": "trigger-probe-enabled"
|
])
|
return
|
}
|
let accessibility = accessibilityStatus()
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.startCheck", [
|
"accessibility": permissionStatusString(accessibility),
|
"alreadyRunning": quickSwitchTriggerService?.isRunning ?? false
|
])
|
guard accessibility == .granted else {
|
stopQuickSwitchTriggerService(reason: "accessibility-not-granted")
|
return
|
}
|
guard quickSwitchTriggerService == nil else {
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.alreadyRunning")
|
return
|
}
|
|
let service = CGEventTapTriggerService(
|
overlayVisibleProvider: { [weak self] in
|
self?.quickSwitchSessionController?.isVisible ?? false
|
},
|
accessibilityTrustedProvider: { [weak self] in
|
self?.accessibilityStatus() == .granted
|
}
|
)
|
service.onQuickSwitch = { [weak self] in
|
self?.openQuickSwitchFromTrigger()
|
}
|
service.onStartFailure = { error in
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.startFailure", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 Quick Switch trigger service failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
}
|
service.start()
|
|
if service.isRunning {
|
quickSwitchTriggerService = service
|
}
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.startResult", [
|
"isRunning": service.isRunning
|
])
|
}
|
|
private func stopQuickSwitchTriggerService(reason: String) {
|
guard let quickSwitchTriggerService else {
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.stopSkipped", [
|
"reason": reason,
|
"state": "not-running"
|
])
|
return
|
}
|
|
quickSwitchTriggerService.stop()
|
self.quickSwitchTriggerService = nil
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.stopped", [
|
"reason": reason
|
])
|
fputs("Round01 Quick Switch trigger service stopped: \(reason)\n", stderr)
|
}
|
|
private func showQuickSwitchIfRequested() {
|
guard round1QuickSwitchOptions.openQuickSwitch || round1QuickSwitchOptions.lifecycleCycles != nil else { return }
|
DevelopmentDiagnostics.log("app.quickSwitch.launchOptionRequested", [
|
"openQuickSwitch": round1QuickSwitchOptions.openQuickSwitch,
|
"lifecycleCycles": round1QuickSwitchOptions.lifecycleCycles
|
])
|
|
if let lifecycleCycles = round1QuickSwitchOptions.lifecycleCycles {
|
runRound1QuickSwitchLifecycle(cycles: lifecycleCycles)
|
return
|
}
|
|
openQuickSwitchFromTrigger()
|
writeQuickSwitchReportIfNeeded()
|
|
if let autoHideAfter = round1QuickSwitchOptions.autoHideAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + autoHideAfter) { [weak self] in
|
self?.hideQuickSwitchSession(reason: .timeout)
|
}
|
}
|
|
if let quitAfter = round1QuickSwitchOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) {
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func runRound1QuickSwitchLifecycle(cycles: Int) {
|
DevelopmentDiagnostics.log("app.quickSwitch.lifecycle.start", [
|
"cycles": cycles
|
])
|
isRunningQuickSwitchLifecycle = true
|
if quickSwitchSessionController == nil {
|
let controller = QuickSwitchSessionController(
|
snapshotLoader: quickSwitchSnapshotLoader(),
|
windowActivationService: quickSwitchWindowActivationService(),
|
windowCloseService: quickSwitchWindowCloseService(),
|
disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh,
|
closeConfirmationRequired: quickSwitchConfirmBeforeClose,
|
onCloseConfirmationDisabled: { [weak self] in
|
self?.setQuickSwitchConfirmBeforeClose(false)
|
},
|
debugHoveredAppGroupIndex: round1QuickSwitchOptions.debugHoveredAppGroupIndex,
|
debugOverlayWidth: round1QuickSwitchOptions.debugOverlayWidth,
|
debugKeySequence: round1QuickSwitchOptions.debugKeySequence,
|
debugMouseSequence: round1QuickSwitchOptions.debugMouseSequence,
|
debugSystemCriticalAfter: round1QuickSwitchOptions.debugSystemCriticalAfter
|
)
|
controller.onSnapshotUpdated = { [weak self] in
|
self?.writeQuickSwitchReportIfNeeded()
|
}
|
quickSwitchSessionController = controller
|
}
|
|
quickSwitchSessionController?.prepareLifecycleRun(targetCycles: cycles)
|
runRound1QuickSwitchLifecycleCycle(currentCycle: 0, totalCycles: max(0, cycles))
|
}
|
|
private func runRound1QuickSwitchLifecycleCycle(currentCycle: Int, totalCycles: Int) {
|
guard currentCycle < totalCycles else {
|
DevelopmentDiagnostics.log("app.quickSwitch.lifecycle.complete", [
|
"totalCycles": totalCycles
|
])
|
quickSwitchSessionController?.finalizeLifecycleRun()
|
isRunningQuickSwitchLifecycle = false
|
writeQuickSwitchReportIfNeeded()
|
NSApp.terminate(nil)
|
return
|
}
|
|
openQuickSwitchSession()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + round1QuickSwitchOptions.lifecycleVisibleDuration) { [weak self] in
|
guard let self else { return }
|
self.hideQuickSwitchSession(reason: .timeout)
|
self.quickSwitchSessionController?.markLifecycleCycleCompleted()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + self.round1QuickSwitchOptions.lifecycleInterval) { [weak self] in
|
self?.runRound1QuickSwitchLifecycleCycle(currentCycle: currentCycle + 1, totalCycles: totalCycles)
|
}
|
}
|
}
|
|
private func openQuickSwitchFromTrigger() {
|
let accessibility = accessibilityStatus()
|
DevelopmentDiagnostics.log("app.quickSwitch.openFromTrigger", [
|
"accessibility": permissionStatusString(accessibility),
|
"screenRecording": permissionStatusString(screenRecordingStatus()),
|
"alreadyVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
guard accessibility == .granted else {
|
stopQuickSwitchTriggerService(reason: "quick-switch-trigger-without-accessibility")
|
hideQuickSwitchSession(reason: .userClosed)
|
showPermissions()
|
return
|
}
|
|
if quickSwitchSessionController?.isVisible == true {
|
quickSwitchSessionController?.retriggerFromShortcut()
|
} else {
|
openQuickSwitchSession()
|
}
|
}
|
|
private func openQuickSwitchSession() {
|
if quickSwitchSessionController == nil {
|
DevelopmentDiagnostics.log("app.quickSwitch.createSessionController")
|
let controller = QuickSwitchSessionController(
|
snapshotLoader: quickSwitchSnapshotLoader(),
|
windowActivationService: quickSwitchWindowActivationService(),
|
windowCloseService: quickSwitchWindowCloseService(),
|
disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh,
|
closeConfirmationRequired: quickSwitchConfirmBeforeClose,
|
onCloseConfirmationDisabled: { [weak self] in
|
self?.setQuickSwitchConfirmBeforeClose(false)
|
},
|
debugHoveredAppGroupIndex: round1QuickSwitchOptions.debugHoveredAppGroupIndex,
|
debugOverlayWidth: round1QuickSwitchOptions.debugOverlayWidth,
|
debugKeySequence: round1QuickSwitchOptions.debugKeySequence,
|
debugMouseSequence: round1QuickSwitchOptions.debugMouseSequence,
|
debugSystemCriticalAfter: round1QuickSwitchOptions.debugSystemCriticalAfter
|
)
|
controller.onSnapshotUpdated = { [weak self] in
|
self?.writeQuickSwitchReportIfNeeded()
|
}
|
quickSwitchSessionController = controller
|
}
|
|
DevelopmentDiagnostics.log("app.quickSwitch.session.showRequested")
|
quickSwitchSessionController?.show()
|
writeQuickSwitchReportIfNeeded()
|
}
|
|
private func hideQuickSwitchSession(reason: DismissReason = .userClosed) {
|
DevelopmentDiagnostics.log("app.quickSwitch.session.hideRequested", [
|
"reason": Self.dismissReasonString(reason),
|
"wasVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
quickSwitchSessionController?.hide(reason: reason)
|
}
|
|
private func writeQuickSwitchReportIfNeeded() {
|
guard let reportPath = round1QuickSwitchOptions.reportPath else { return }
|
guard !isRunningQuickSwitchLifecycle else { return }
|
let reportPathSummary = DevelopmentDiagnostics.pathSummary(reportPath)
|
|
let report = quickSwitchSessionController?.reportDictionary() ?? [
|
"snapshotLoaded": false,
|
"quickSwitchVisible": false,
|
"permissionStatus": "\(accessibilityStatus())",
|
"diagnosticLogLocationKind": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["locationKind"] ?? "unknown",
|
"diagnosticLogBasename": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["basename"] ?? "aligner-dev.log"
|
]
|
|
do {
|
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
|
try data.write(to: URL(fileURLWithPath: reportPath), options: [.atomic])
|
DevelopmentDiagnostics.log("app.quickSwitch.reportWritten", [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
])
|
} catch {
|
var fields: [String: CustomStringConvertible?] = [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
]
|
DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
|
DevelopmentDiagnostics.log("app.quickSwitch.reportFailed", fields)
|
fputs("Round01 Quick Switch report failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
}
|
}
|
|
private func accessibilityStatus() -> PermissionStatus {
|
if round1QuickSwitchOptions.simulateAccessibilityDenied {
|
return .notGranted
|
}
|
|
return permissionChecker.status(for: .accessibility)
|
}
|
|
private func screenRecordingStatus() -> PermissionStatus {
|
if round1QuickSwitchOptions.simulateScreenRecordingDenied {
|
return .notGranted
|
}
|
|
return permissionChecker.status(for: .screenRecording)
|
}
|
|
private func quickSwitchSnapshotLoader() -> any QuickSwitchSnapshotLoading {
|
if round1QuickSwitchOptions.fixtureActivation {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 2, windowsPerApp: 1, mode: .activation)
|
}
|
|
if round1QuickSwitchOptions.fixtureCandidateFiltering {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .candidateFiltering)
|
}
|
|
if let fixtureAppCount = round1QuickSwitchOptions.fixtureAppCount {
|
return FixtureQuickSwitchSnapshotLoader(
|
appCount: fixtureAppCount,
|
windowsPerApp: round1QuickSwitchOptions.fixtureWindowsPerApp ?? 1,
|
mode: .layout,
|
usesMultipleDisplays: round1QuickSwitchOptions.fixtureMultiDisplay
|
)
|
}
|
|
let liveLoader = LiveQuickSwitchSnapshotLoader()
|
guard let delay = round1QuickSwitchOptions.snapshotLoadDelay else {
|
return liveLoader
|
}
|
|
return DelayedQuickSwitchSnapshotLoader(base: liveLoader, delay: delay)
|
}
|
|
private func quickSwitchWindowActivationService() -> any WindowActivationServiceProtocol {
|
if round1QuickSwitchOptions.debugWindowActivation {
|
return DebugWindowActivationService()
|
}
|
|
return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] })
|
}
|
|
private func quickSwitchWindowCloseService() -> any WindowCloseServiceProtocol {
|
if round1QuickSwitchOptions.debugWindowClose {
|
return DebugWindowCloseService()
|
}
|
|
return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] })
|
}
|
|
private var quickSwitchConfirmBeforeClose: Bool {
|
let value = UserDefaults.standard.object(forKey: Self.quickSwitchConfirmBeforeCloseDefaultsKey)
|
return (value as? Bool) ?? true
|
}
|
|
private func setQuickSwitchConfirmBeforeClose(_ enabled: Bool) {
|
UserDefaults.standard.set(enabled, forKey: Self.quickSwitchConfirmBeforeCloseDefaultsKey)
|
DevelopmentDiagnostics.log("app.quickSwitch.closeConfirmationPreferenceChanged", [
|
"confirmBeforeClose": enabled
|
])
|
}
|
|
@objc private func showPermissions() {
|
DevelopmentDiagnostics.log("app.permissions.show", [
|
"accessibility": permissionStatusString(accessibilityStatus()),
|
"screenRecording": permissionStatusString(screenRecordingStatus())
|
])
|
NSApp.setActivationPolicy(.regular)
|
stopQuickSwitchTriggerService(reason: "show-permissions")
|
hideQuickSwitchSession(reason: .userClosed)
|
|
if permissionWindowController == nil {
|
let controller = PermissionsWindowController(permissionManager: permissionChecker)
|
controller.onPermissionStatusChanged = { [weak self] kind, status in
|
DevelopmentDiagnostics.log("app.permissions.statusChanged", [
|
"kind": kind.rawValue,
|
"status": self?.permissionStatusString(status) ?? "\(status)"
|
])
|
guard kind == .accessibility else { return }
|
|
switch status {
|
case .granted:
|
self?.startQuickSwitchTriggerServiceIfPossible()
|
case .notGranted:
|
self?.stopQuickSwitchTriggerService(reason: "accessibility-status-not-granted")
|
self?.hideQuickSwitchSession(reason: .userClosed)
|
}
|
}
|
controller.onPermissionRequestStarted = { [weak self] kind in
|
DevelopmentDiagnostics.log("app.permissions.requestStarted", [
|
"kind": kind.rawValue
|
])
|
if kind == .accessibility {
|
self?.stopQuickSwitchTriggerService(reason: "accessibility-request-started")
|
self?.hideQuickSwitchSession(reason: .userClosed)
|
}
|
}
|
permissionWindowController = controller
|
}
|
|
permissionWindowController?.showWindow(nil)
|
permissionWindowController?.window?.makeKeyAndOrderFront(nil)
|
NSApp.activate(ignoringOtherApps: true)
|
DevelopmentDiagnostics.log("app.permissions.windowShown", [
|
"activationPolicy": NSApp.activationPolicy().rawValue,
|
"windowVisible": permissionWindowController?.window?.isVisible ?? false
|
])
|
}
|
|
@objc private func showRound0DebugOverlay() {
|
DevelopmentDiagnostics.log("app.menu.showRound0DebugOverlay")
|
showDebugOverlay(includeSettingsChild: true)
|
}
|
|
@objc private func hideRound0DebugOverlay() {
|
DevelopmentDiagnostics.log("app.menu.hideRound0DebugOverlay")
|
hideDebugOverlay()
|
}
|
|
@objc private func showQuickSwitchFromMenu() {
|
DevelopmentDiagnostics.log("app.menu.showQuickSwitch")
|
openQuickSwitchFromTrigger()
|
}
|
|
@objc private func hideQuickSwitchFromMenu() {
|
DevelopmentDiagnostics.log("app.menu.hideQuickSwitch")
|
hideQuickSwitchSession()
|
}
|
|
private func hideRound1PerformancePoC() {
|
round1PerformancePoCController?.hide()
|
}
|
|
private func writeRound1PerformancePoCReportIfNeeded() {
|
guard let reportPath = round1PoCOptions.reportPath,
|
let report = round1PerformancePoCController?.reportDictionary()
|
else { return }
|
let reportPathSummary = DevelopmentDiagnostics.pathSummary(reportPath)
|
|
do {
|
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
|
try data.write(to: URL(fileURLWithPath: reportPath), options: [.atomic])
|
DevelopmentDiagnostics.log("app.performancePoC.reportWritten", [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
])
|
} catch {
|
var fields: [String: CustomStringConvertible?] = [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
]
|
DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
|
DevelopmentDiagnostics.log("app.performancePoC.reportFailed", fields)
|
fputs("Round01 performance PoC report failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
}
|
}
|
|
private func showDebugOverlay(includeSettingsChild: Bool) {
|
DevelopmentDiagnostics.log("app.debugOverlay.show", [
|
"includeSettingsChild": includeSettingsChild
|
])
|
if debugOverlayController == nil {
|
debugOverlayController = Round0DebugOverlayController()
|
}
|
|
debugOverlayController?.show(includeSettingsChild: includeSettingsChild)
|
}
|
|
private func hideDebugOverlay() {
|
DevelopmentDiagnostics.log("app.debugOverlay.hide")
|
debugOverlayController?.hide()
|
}
|
|
private func openRound1PerformancePoC(autoCycle: Bool) {
|
DevelopmentDiagnostics.log("app.performancePoC.show", [
|
"autoCycle": autoCycle
|
])
|
if round1PerformancePoCController == nil {
|
round1PerformancePoCController = Round1PerformancePoCController()
|
}
|
|
round1PerformancePoCController?.show(autoCycle: autoCycle)
|
}
|
|
@objc private func quit() {
|
DevelopmentDiagnostics.logSync("app.menu.quit")
|
NSApp.terminate(nil)
|
}
|
|
private func permissionStatusString(_ status: PermissionStatus) -> String {
|
switch status {
|
case .granted:
|
return "granted"
|
case .notGranted:
|
return "notGranted"
|
}
|
}
|
|
private static func dismissReasonString(_ reason: DismissReason) -> String {
|
switch reason {
|
case .escape:
|
return "escape"
|
case .focusLost:
|
return "focusLost"
|
case .systemCriticalWindow:
|
return "systemCriticalWindow"
|
case .timeout:
|
return "timeout"
|
case .userClosed:
|
return "userClosed"
|
}
|
}
|
}
|