@lattices/cli 0.4.2 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/AppShell/App.swift +20 -0
  7. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
  8. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
  9. package/app/Sources/AppShell/AppUpdater.swift +92 -0
  10. package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
  11. package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
  12. package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
  13. package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
  14. package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
  15. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
  16. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
  17. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
  18. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
  19. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  20. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  21. package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
  22. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  23. package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
  24. package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
  25. package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
  26. package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
  27. package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
  28. package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
  29. package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
  30. package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
  31. package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
  32. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
  33. package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
  34. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  35. package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
  36. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  37. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  38. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  39. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
  40. package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
  41. package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
  42. package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
  43. package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
  44. package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
  45. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  46. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
  47. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  48. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  49. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  50. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  51. package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
  52. package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
  53. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  54. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
  55. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
  56. package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
  57. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
  58. package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
  59. package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
  60. package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
  61. package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
  62. package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
  63. package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
  64. package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
  65. package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
  66. package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
  67. package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
  68. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
  69. package/bin/assistant-intelligence.ts +874 -0
  70. package/bin/handsoff-infer.ts +16 -209
  71. package/bin/handsoff-worker.ts +45 -258
  72. package/bin/lattices-app.ts +62 -0
  73. package/bin/lattices-dev +4 -0
  74. package/bin/lattices.ts +125 -14
  75. package/docs/agents.md +14 -0
  76. package/docs/api.md +55 -0
  77. package/docs/app.md +3 -0
  78. package/docs/companion-deck.md +180 -0
  79. package/docs/component-extraction-roadmap.md +392 -0
  80. package/docs/config.md +25 -0
  81. package/docs/tiling-reference.md +55 -0
  82. package/docs/voice-error-model.md +73 -0
  83. package/package.json +4 -1
  84. package/app/Sources/App.swift +0 -10
  85. package/app/Sources/CommandPaletteWindow.swift +0 -134
  86. package/app/Sources/MouseFinder.swift +0 -222
  87. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  88. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  89. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  90. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  91. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  92. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  93. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  94. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  95. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  96. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  97. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  98. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  99. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  100. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  101. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  102. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  103. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  104. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  105. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  106. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  107. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  108. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  109. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  110. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  111. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  112. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  113. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  114. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  115. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  116. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  117. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  118. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  119. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  120. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  121. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  122. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  123. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  124. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  125. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  126. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  127. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  128. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  129. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  130. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  131. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  132. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  133. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  134. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  135. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  136. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  137. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  138. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  139. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  140. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  141. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  142. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  143. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  144. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  145. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  146. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -51,10 +51,10 @@ final class WindowHighlight {
51
51
  private var fadeTimer: Timer?
52
52
 
53
53
  /// Flash a green border overlay at the given screen frame
54
- func flash(frame: NSRect, duration: TimeInterval = 1.2) {
54
+ func flash(frame: NSRect, duration: TimeInterval = 0.9) {
55
55
  dismiss()
56
56
 
57
- let inset: CGFloat = -8 // slightly larger than the window
57
+ let inset: CGFloat = -6 // slightly larger than the window
58
58
  let expandedFrame = frame.insetBy(dx: inset, dy: inset)
59
59
 
60
60
  let window = NSWindow(
@@ -110,22 +110,28 @@ final class WindowHighlight {
110
110
 
111
111
  private class HighlightBorderView: NSView {
112
112
  override func draw(_ dirtyRect: NSRect) {
113
- let borderWidth: CGFloat = 4
113
+ let borderWidth: CGFloat = 3
114
114
  let cornerRadius: CGFloat = 12
115
115
 
116
116
  // Outer glow
117
117
  let glowRect = bounds.insetBy(dx: 1, dy: 1)
118
118
  let glowPath = NSBezierPath(roundedRect: glowRect, xRadius: cornerRadius + 2, yRadius: cornerRadius + 2)
119
- glowPath.lineWidth = borderWidth + 4
120
- NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.15).setStroke()
119
+ glowPath.lineWidth = borderWidth + 2
120
+ NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.07).setStroke()
121
121
  glowPath.stroke()
122
122
 
123
123
  // Main border
124
124
  let rect = bounds.insetBy(dx: borderWidth / 2 + 2, dy: borderWidth / 2 + 2)
125
125
  let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
126
126
  path.lineWidth = borderWidth
127
- NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.9).setStroke()
127
+ NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.58).setStroke()
128
128
  path.stroke()
129
+
130
+ let innerRect = rect.insetBy(dx: 3, dy: 3)
131
+ let innerPath = NSBezierPath(roundedRect: innerRect, xRadius: max(cornerRadius - 3, 6), yRadius: max(cornerRadius - 3, 6))
132
+ innerPath.lineWidth = 1
133
+ NSColor.white.withAlphaComponent(0.10).setStroke()
134
+ innerPath.stroke()
129
135
  }
130
136
  }
131
137
 
@@ -588,20 +594,84 @@ enum WindowTiler {
588
594
  return Int(getActive(mainConn()))
589
595
  }
590
596
 
591
- /// Find a window by its title tag and return its CGWindowID and owner PID
592
- static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
593
- guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
594
- return nil
597
+ static func adjacentSpace(in spaces: [SpaceInfo], currentSpaceId: Int, offset: Int) -> SpaceInfo? {
598
+ guard let currentIndex = spaces.firstIndex(where: { $0.id == currentSpaceId }) else { return nil }
599
+ let targetIndex = currentIndex + offset
600
+ guard spaces.indices.contains(targetIndex) else { return nil }
601
+ return spaces[targetIndex]
602
+ }
603
+
604
+ private struct AdjacentSpaceContext {
605
+ let point: CGPoint
606
+ let display: DisplaySpaces
607
+ let activeSpaceId: Int
608
+ let currentSpaceId: Int
609
+ let target: SpaceInfo?
610
+ }
611
+
612
+ private static func adjacentSpaceContext(offset: Int, from cgPoint: CGPoint? = nil) -> AdjacentSpaceContext? {
613
+ let point = cgPoint ?? currentMouseCGPoint()
614
+ guard let display = displaySpaces(containing: point) else { return nil }
615
+ let activeSpaceId = getCurrentSpace()
616
+ let currentSpaceId = resolvedCurrentSpaceId(for: display, activeSpaceId: activeSpaceId)
617
+ let target = adjacentSpace(in: display.spaces, currentSpaceId: currentSpaceId, offset: offset)
618
+ return AdjacentSpaceContext(
619
+ point: point,
620
+ display: display,
621
+ activeSpaceId: activeSpaceId,
622
+ currentSpaceId: currentSpaceId,
623
+ target: target
624
+ )
625
+ }
626
+
627
+ static func adjacentSpaceTarget(offset: Int, from cgPoint: CGPoint? = nil) -> SpaceInfo? {
628
+ adjacentSpaceContext(offset: offset, from: cgPoint)?.target
629
+ }
630
+
631
+ @discardableResult
632
+ static func switchToAdjacentSpace(offset: Int, from cgPoint: CGPoint? = nil) -> Bool {
633
+ guard let context = adjacentSpaceContext(offset: offset, from: cgPoint) else {
634
+ DiagnosticLog.shared.warn("switchToAdjacentSpace: no adjacent space for offset \(offset) from \(formatCGPoint(cgPoint ?? currentMouseCGPoint()))")
635
+ return false
595
636
  }
596
- for info in windowList {
597
- if let name = info[kCGWindowName as String] as? String,
598
- name.contains(tag),
599
- let wid = info[kCGWindowNumber as String] as? UInt32,
600
- let pid = info[kCGWindowOwnerPID as String] as? pid_t {
601
- return (wid, pid)
637
+
638
+ let spaces = context.display.spaces.map(\.id)
639
+ let targetText = context.target.map { String($0.id) } ?? "none"
640
+ DiagnosticLog.shared.info(
641
+ "switchToAdjacentSpace: offset=\(offset) point=\(formatCGPoint(context.point)) displayId=\(context.display.displayId) active=\(context.activeSpaceId) displayCurrent=\(context.display.currentSpaceId) resolved=\(context.currentSpaceId) target=\(targetText) spaces=\(spaces)"
642
+ )
643
+
644
+ if getDisplaySpaces().count == 1 {
645
+ if let finalSpaceId = switchToAdjacentSpaceViaSystemShortcut(
646
+ offset: offset,
647
+ displayId: context.display.displayId,
648
+ initialSpaceId: context.currentSpaceId
649
+ ) {
650
+ if let target = context.target, finalSpaceId != target.id {
651
+ DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut changed \(context.currentSpaceId) → \(finalSpaceId) (expected \(target.id))")
652
+ } else {
653
+ DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut changed \(context.currentSpaceId) → \(finalSpaceId)")
654
+ }
655
+ return true
656
+ }
657
+
658
+ guard let target = context.target else {
659
+ DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId) and there is no adjacent space")
660
+ return false
602
661
  }
662
+
663
+ DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId), falling back to SkyLight target \(target.id)")
603
664
  }
604
- return nil
665
+
666
+ guard let target = context.target else { return false }
667
+ let switched = switchToSpace(spaceId: target.id)
668
+ DiagnosticLog.shared.info("switchToAdjacentSpace: SkyLight \(switched ? "reached" : "missed") target \(target.id)")
669
+ return switched
670
+ }
671
+
672
+ /// Find a window by its title tag and return its CGWindowID and owner PID
673
+ static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
674
+ SessionWindowLocator.findCGWindow(tag: tag).map { ($0.wid, $0.pid) }
605
675
  }
606
676
 
607
677
  /// Get the space ID(s) a window is on
@@ -614,10 +684,12 @@ enum WindowTiler {
614
684
  return result.map { $0.intValue }
615
685
  }
616
686
 
617
- /// Switch a display to a specific Space
618
- static func switchToSpace(spaceId: Int) {
687
+ /// Switch a display to a specific Space.
688
+ /// Returns true once the requested Space becomes current.
689
+ @discardableResult
690
+ static func switchToSpace(spaceId: Int, verify: Bool = true) -> Bool {
619
691
  guard let mainConn = CGS.mainConnectionID,
620
- let setSpace = CGS.setCurrentSpace else { return }
692
+ let setSpace = CGS.setCurrentSpace else { return false }
621
693
 
622
694
  let cid = mainConn()
623
695
 
@@ -625,10 +697,37 @@ enum WindowTiler {
625
697
  let allDisplays = getDisplaySpaces()
626
698
  for display in allDisplays {
627
699
  if display.spaces.contains(where: { $0.id == spaceId }) {
700
+ let initialSpace = display.currentSpaceId
701
+ if initialSpace == spaceId {
702
+ DiagnosticLog.shared.info("switchToSpace: display \(display.displayIndex) already on target \(spaceId)")
703
+ return true
704
+ }
705
+
706
+ DiagnosticLog.shared.info(
707
+ "switchToSpace: requesting \(spaceId) on display \(display.displayIndex) id=\(display.displayId) from \(initialSpace)"
708
+ )
628
709
  setSpace(cid, display.displayId as CFString, UInt64(spaceId))
629
- return
710
+ guard verify else { return true }
711
+
712
+ let deadline = Date().addingTimeInterval(0.45)
713
+ while Date() < deadline {
714
+ usleep(30_000)
715
+ let current = getDisplaySpaces().first(where: { $0.displayId == display.displayId })?.currentSpaceId ?? 0
716
+ if current == spaceId {
717
+ DiagnosticLog.shared.info("switchToSpace: display \(display.displayIndex) confirmed target \(spaceId)")
718
+ return true
719
+ }
720
+ }
721
+
722
+ DiagnosticLog.shared.warn(
723
+ "switchToSpace: requested \(spaceId) on display \(display.displayIndex) from \(initialSpace), but current Space did not change"
724
+ )
725
+ return false
630
726
  }
631
727
  }
728
+
729
+ DiagnosticLog.shared.warn("switchToSpace: couldn't resolve display for space \(spaceId)")
730
+ return false
632
731
  }
633
732
 
634
733
  // MARK: - Move Window Between Spaces
@@ -650,13 +749,9 @@ enum WindowTiler {
650
749
 
651
750
  // Find the window — CG first, then AX→CG fallback
652
751
  let wid: UInt32
653
- if let (w, _) = findWindow(tag: tag) {
654
- wid = w
655
- diag.info("moveWindowToSpace: found via CG wid=\(w)")
656
- } else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
657
- let w = matchCGWindow(pid: pid, axWindow: axWindow) {
658
- wid = w
659
- diag.info("moveWindowToSpace: found via AX→CG wid=\(w)")
752
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
753
+ wid = match.wid
754
+ diag.info("moveWindowToSpace: located wid=\(match.wid) pid=\(match.pid)")
660
755
  } else {
661
756
  diag.warn("moveWindowToSpace: window not found for tag \(tag) — switching view only")
662
757
  switchToSpace(spaceId: spaceId)
@@ -724,39 +819,31 @@ enum WindowTiler {
724
819
  let tag = Terminal.windowTag(for: session)
725
820
 
726
821
  // Path 1: CG window lookup (needs Screen Recording permission for window names)
727
- if let (wid, pid) = findWindow(tag: tag) {
728
- diag.success("Path 1 (CG): found wid=\(wid) pid=\(pid)")
729
- navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
822
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
823
+ diag.success("Path 1/2 (locator): found wid=\(match.wid) pid=\(match.pid)")
824
+ navigateToKnownWindow(wid: match.wid, pid: match.pid, tag: tag, session: session, terminal: terminal)
730
825
  diag.finish(t)
731
826
  return
732
827
  }
733
- diag.warn("Path 1 (CG): findWindow failed — no Screen Recording?")
828
+ diag.warn("SessionWindowLocator failed — trying direct AX fallback")
734
829
 
735
- // Path 2: AX API fallback (needs Accessibility permission)
736
- if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag) {
737
- diag.success("Path 2 (AX): found window for \(terminal.rawValue) pid=\(pid)")
738
- // Try to match AX window → CG window for space switching
739
- if let wid = matchCGWindow(pid: pid, axWindow: axWindow) {
740
- diag.success("Path 2 (AX→CG): matched CG wid=\(wid)")
741
- navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
830
+ if let (pid, axWindow) = SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag) {
831
+ diag.success("Direct AX fallback: found window for \(terminal.rawValue) pid=\(pid)")
832
+ AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
833
+ AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
834
+ if let app = NSRunningApplication(processIdentifier: pid) {
835
+ app.activate()
836
+ }
837
+ if let frame = axWindowFrame(axWindow) {
838
+ diag.info("Highlighting via AX frame: \(frame)")
839
+ DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
742
840
  } else {
743
- diag.warn("Path 2 (AX): no CG match raising without space switch")
744
- AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
745
- AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
746
- if let app = NSRunningApplication(processIdentifier: pid) {
747
- app.activate()
748
- }
749
- if let frame = axWindowFrame(axWindow) {
750
- diag.info("Highlighting via AX frame: \(frame)")
751
- DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
752
- } else {
753
- diag.error("axWindowFrame returned nil — no highlight")
754
- }
841
+ diag.error("axWindowFrame returned nilno highlight")
755
842
  }
756
843
  diag.finish(t)
757
844
  return
758
845
  }
759
- diag.warn("Path 2 (AX): findWindowViaAX failed — no Accessibility?")
846
+ diag.warn("Direct AX fallback failed — no Accessibility?")
760
847
 
761
848
  // Path 3: AppleScript / bare activate fallback
762
849
  diag.warn("Path 3: falling back to AppleScript/activate")
@@ -784,70 +871,12 @@ enum WindowTiler {
784
871
  }
785
872
  }
786
873
 
787
- /// Find a terminal window by title tag using AX API (requires Accessibility permission)
788
874
  private static func findWindowViaAX(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
789
- let diag = DiagnosticLog.shared
790
- guard let app = NSWorkspace.shared.runningApplications.first(where: {
791
- $0.bundleIdentifier == terminal.bundleId
792
- }) else {
793
- diag.error("findWindowViaAX: \(terminal.rawValue) (\(terminal.bundleId)) not running")
794
- return nil
795
- }
796
-
797
- let pid = app.processIdentifier
798
- let appRef = AXUIElementCreateApplication(pid)
799
- var windowsRef: CFTypeRef?
800
- let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
801
- guard err == .success, let windows = windowsRef as? [AXUIElement] else {
802
- diag.error("findWindowViaAX: AX error \(err.rawValue) — Accessibility not granted?")
803
- return nil
804
- }
805
-
806
- diag.info("findWindowViaAX: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
807
- for win in windows {
808
- var titleRef: CFTypeRef?
809
- AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
810
- let title = titleRef as? String ?? "<no title>"
811
- if title.contains(tag) {
812
- diag.success("findWindowViaAX: matched \"\(title)\"")
813
- return (pid, win)
814
- } else {
815
- diag.info(" skip: \"\(title)\"")
816
- }
817
- }
818
- diag.warn("findWindowViaAX: no window matched tag \(tag)")
819
- return nil
875
+ SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag)
820
876
  }
821
877
 
822
- /// Match an AX window to its CG window ID using PID + bounds comparison
823
878
  private static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
824
- var posRef: CFTypeRef?
825
- var sizeRef: CFTypeRef?
826
- AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
827
- AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
828
- guard let pv = posRef, let sv = sizeRef else { return nil }
829
-
830
- var pos = CGPoint.zero
831
- var size = CGSize.zero
832
- AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
833
- AXValueGetValue(sv as! AXValue, .cgSize, &size)
834
-
835
- guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return nil }
836
-
837
- for info in windowList {
838
- guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
839
- wPid == pid,
840
- let wid = info[kCGWindowNumber as String] as? UInt32,
841
- let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
842
- var rect = CGRect.zero
843
- if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
844
- if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
845
- abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
846
- return wid
847
- }
848
- }
849
- }
850
- return nil
879
+ SessionWindowLocator.matchCGWindow(pid: pid, axWindow: axWindow)
851
880
  }
852
881
 
853
882
  /// Get NSRect from an AX window element (AX uses top-left origin, convert to NS bottom-left)
@@ -997,11 +1026,8 @@ enum WindowTiler {
997
1026
 
998
1027
  // Find the window
999
1028
  let wid: UInt32
1000
- if let (w, _) = findWindow(tag: tag) {
1001
- wid = w
1002
- } else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
1003
- let w = matchCGWindow(pid: pid, axWindow: axWindow) {
1004
- wid = w
1029
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
1030
+ wid = match.wid
1005
1031
  } else {
1006
1032
  return nil
1007
1033
  }
@@ -1206,14 +1232,11 @@ enum WindowTiler {
1206
1232
  }
1207
1233
 
1208
1234
  /// Distribute ALL visible non-Lattices windows into a smart grid on the screen with the most windows.
1209
- static func distributeVisible() {
1235
+ static func distributeVisible(reactivateLattices: Bool = true) {
1210
1236
  let diag = DiagnosticLog.shared
1211
1237
  let t = diag.startTimed("distributeVisible")
1212
1238
 
1213
- let allEntries = DesktopModel.shared.allWindows()
1214
- let visible = allEntries.filter { entry in
1215
- entry.isOnScreen && entry.app != "Lattices" && entry.frame.w > 50 && entry.frame.h > 50
1216
- }
1239
+ let visible = visibleDistributableWindows()
1217
1240
 
1218
1241
  guard !visible.isEmpty else {
1219
1242
  diag.info("distributeVisible: no visible windows to distribute")
@@ -1223,7 +1246,59 @@ enum WindowTiler {
1223
1246
 
1224
1247
  let windows = visible.map { (wid: $0.wid, pid: $0.pid) }
1225
1248
  diag.info("distributeVisible: \(windows.count) windows")
1226
- batchRaiseAndDistribute(windows: windows)
1249
+ batchRaiseAndDistribute(windows: windows, reactivateLattices: reactivateLattices)
1250
+ diag.finish(t)
1251
+ }
1252
+
1253
+ /// Distribute visible windows matching the frontmost app's broader type.
1254
+ /// Example: when the active app is a terminal, grid all visible terminal windows on that display.
1255
+ static func distributeVisibleByFrontmostType(reactivateLattices: Bool = true) {
1256
+ let diag = DiagnosticLog.shared
1257
+ let t = diag.startTimed("distributeVisibleByFrontmostType")
1258
+
1259
+ let visible = visibleDistributableWindows()
1260
+ guard !visible.isEmpty else {
1261
+ diag.info("distributeVisibleByFrontmostType: no visible windows to distribute")
1262
+ diag.finish(t)
1263
+ return
1264
+ }
1265
+
1266
+ let frontmostAppName = NSWorkspace.shared.frontmostApplication?.localizedName
1267
+ let anchor = frontmostAppName.flatMap { name in
1268
+ visible.first { $0.app.localizedCaseInsensitiveCompare(name) == .orderedSame }
1269
+ } ?? visible.first
1270
+
1271
+ guard let anchor else {
1272
+ diag.info("distributeVisibleByFrontmostType: no anchor window resolved")
1273
+ diag.finish(t)
1274
+ return
1275
+ }
1276
+
1277
+ let grouping = AppTypeClassifier.grouping(for: anchor.app)
1278
+ let anchorScreen = screenForWindowFrame(anchor.frame)
1279
+ let anchorScreenId = screenID(for: anchorScreen)
1280
+
1281
+ let sameScreenMatches = visible.filter { entry in
1282
+ AppTypeClassifier.matches(entry.app, grouping: grouping) &&
1283
+ screenID(for: screenForWindowFrame(entry.frame)) == anchorScreenId
1284
+ }
1285
+
1286
+ let matches = sameScreenMatches.isEmpty
1287
+ ? visible.filter { AppTypeClassifier.matches($0.app, grouping: grouping) }
1288
+ : sameScreenMatches
1289
+
1290
+ guard !matches.isEmpty else {
1291
+ diag.info("distributeVisibleByFrontmostType: no matches for \(grouping.label)")
1292
+ diag.finish(t)
1293
+ return
1294
+ }
1295
+
1296
+ let ordered = sortWindowsForGrid(matches)
1297
+ diag.info("distributeVisibleByFrontmostType: grouping=\(grouping.label) count=\(ordered.count) screen=\(anchorScreen.localizedName)")
1298
+ batchRaiseAndDistribute(
1299
+ windows: ordered.map { (wid: $0.wid, pid: $0.pid) },
1300
+ reactivateLattices: reactivateLattices
1301
+ )
1227
1302
  diag.finish(t)
1228
1303
  }
1229
1304
 
@@ -1685,7 +1760,11 @@ enum WindowTiler {
1685
1760
 
1686
1761
  /// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process.
1687
1762
  /// If `region` is provided (fractional x, y, w, h), the grid is constrained to that sub-area.
1688
- static func batchRaiseAndDistribute(windows: [(wid: UInt32, pid: Int32)], region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil) {
1763
+ static func batchRaiseAndDistribute(
1764
+ windows: [(wid: UInt32, pid: Int32)],
1765
+ region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil,
1766
+ reactivateLattices: Bool = true
1767
+ ) {
1689
1768
  guard !windows.isEmpty else { return }
1690
1769
  let diag = DiagnosticLog.shared
1691
1770
 
@@ -1744,91 +1823,99 @@ enum WindowTiler {
1744
1823
  }
1745
1824
 
1746
1825
  // Group by pid for AX queries, keep slot mapping
1747
- var widToSlot: [UInt32: Int] = [:]
1748
- for (i, win) in windows.enumerated() { widToSlot[win.wid] = i }
1749
-
1750
- var byPid: [Int32: [(wid: UInt32, target: CGRect)]] = [:]
1751
- for (i, win) in windows.enumerated() {
1752
- byPid[win.pid, default: []].append((wid: win.wid, target: slots[i]))
1826
+ var byPid: [Int32: [(slotIdx: Int, wid: UInt32, target: CGRect)]] = [:]
1827
+ let moves: [(wid: UInt32, pid: Int32, frame: CGRect)] = windows.enumerated().map { index, win in
1828
+ let target = slots[index]
1829
+ byPid[win.pid, default: []].append((slotIdx: index, wid: win.wid, target: target))
1830
+ return (wid: win.wid, pid: win.pid, frame: target)
1753
1831
  }
1754
1832
 
1755
- struct AXWin { let el: AXUIElement; let pos: CGPoint; let size: CGSize }
1756
-
1757
- // Pass 1: Move all windows to target positions (no raise yet)
1833
+ // Pass 1: Move all windows using exact wid→AX mapping.
1758
1834
  var moved = 0
1759
1835
  var failed: [UInt32] = []
1760
1836
  var resolvedAXElements: [(slotIdx: Int, el: AXUIElement)] = [] // for raise pass
1837
+ var activatedPids = Set<Int32>()
1838
+
1839
+ let cid = _SLSMainConnectionID?()
1840
+ if let cid { _ = _SLSDisableUpdate?(cid) }
1761
1841
 
1762
1842
  for (pid, windowMoves) in byPid {
1763
1843
  let appRef = AXUIElementCreateApplication(pid)
1844
+ AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, false as CFTypeRef)
1845
+
1764
1846
  var windowsRef: CFTypeRef?
1765
1847
  let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
1766
1848
  guard err == .success, let axWindows = windowsRef as? [AXUIElement] else {
1767
1849
  diag.warn(" AX query failed for pid=\(pid) err=\(err.rawValue)")
1768
1850
  failed.append(contentsOf: windowMoves.map(\.wid))
1851
+ AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, true as CFTypeRef)
1769
1852
  continue
1770
1853
  }
1771
1854
 
1772
- var axCache: [AXWin] = []
1855
+ var axByWid: [UInt32: AXUIElement] = [:]
1773
1856
  for axWin in axWindows {
1774
- var posRef: CFTypeRef?; var sizeRef: CFTypeRef?
1775
- AXUIElementCopyAttributeValue(axWin, kAXPositionAttribute as CFString, &posRef)
1776
- AXUIElementCopyAttributeValue(axWin, kAXSizeAttribute as CFString, &sizeRef)
1777
- guard let pv = posRef, let sv = sizeRef else { continue }
1778
- var pos = CGPoint.zero; var size = CGSize.zero
1779
- AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
1780
- AXValueGetValue(sv as! AXValue, .cgSize, &size)
1781
- axCache.append(AXWin(el: axWin, pos: pos, size: size))
1857
+ var windowId: CGWindowID = 0
1858
+ if _AXUIElementGetWindow(axWin, &windowId) == .success {
1859
+ axByWid[windowId] = axWin
1860
+ }
1782
1861
  }
1783
1862
 
1784
1863
  for wm in windowMoves {
1785
- guard let cgRect = cgFrames[wm.wid] else {
1786
- diag.warn(" wid=\(wm.wid): no CG frame, skipping")
1787
- failed.append(wm.wid)
1788
- continue
1789
- }
1790
- guard let ax = axCache.first(where: {
1791
- abs(cgRect.origin.x - $0.pos.x) < 2 && abs(cgRect.origin.y - $0.pos.y) < 2 &&
1792
- abs(cgRect.width - $0.size.width) < 2 && abs(cgRect.height - $0.size.height) < 2
1793
- }) else {
1794
- diag.warn(" wid=\(wm.wid): CG frame (\(Int(cgRect.origin.x)),\(Int(cgRect.origin.y)) \(Int(cgRect.width))x\(Int(cgRect.height))) — no AX match among \(axCache.count) AX windows")
1795
- for (j, axw) in axCache.enumerated() {
1796
- diag.info(" AX[\(j)]: pos=(\(Int(axw.pos.x)),\(Int(axw.pos.y))) size=\(Int(axw.size.width))x\(Int(axw.size.height))")
1864
+ guard let axWin = axByWid[wm.wid] else {
1865
+ if let cgRect = cgFrames[wm.wid] {
1866
+ diag.warn(" wid=\(wm.wid): CG frame (\(Int(cgRect.origin.x)),\(Int(cgRect.origin.y)) \(Int(cgRect.width))x\(Int(cgRect.height))) — no AX wid match")
1867
+ } else {
1868
+ diag.warn(" wid=\(wm.wid): no CG frame and no AX wid match")
1797
1869
  }
1798
1870
  failed.append(wm.wid)
1799
1871
  continue
1800
1872
  }
1801
1873
 
1802
- let slotIdx = widToSlot[wm.wid] ?? -1
1803
- // Move only — raise comes later
1804
1874
  var newPos = CGPoint(x: wm.target.origin.x, y: wm.target.origin.y)
1805
1875
  var newSize = CGSize(width: wm.target.width, height: wm.target.height)
1806
- let posOk = AXValueCreate(.cgPoint, &newPos).map {
1807
- AXUIElementSetAttributeValue(ax.el, kAXPositionAttribute as CFString, $0)
1876
+ let sizeErr1 = AXValueCreate(.cgSize, &newSize).map {
1877
+ AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, $0)
1878
+ }
1879
+ let posErr = AXValueCreate(.cgPoint, &newPos).map {
1880
+ AXUIElementSetAttributeValue(axWin, kAXPositionAttribute as CFString, $0)
1808
1881
  }
1809
- let sizeOk = AXValueCreate(.cgSize, &newSize).map {
1810
- AXUIElementSetAttributeValue(ax.el, kAXSizeAttribute as CFString, $0)
1882
+ let sizeErr2 = AXValueCreate(.cgSize, &newSize).map {
1883
+ AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, $0)
1811
1884
  }
1812
- diag.info(" Move[\(slotIdx)] wid=\(wm.wid): target=(\(Int(wm.target.origin.x)),\(Int(wm.target.origin.y))) \(Int(wm.target.width))x\(Int(wm.target.height)) posErr=\(posOk?.rawValue ?? -1) sizeErr=\(sizeOk?.rawValue ?? -1)")
1813
- resolvedAXElements.append((slotIdx: slotIdx, el: ax.el))
1885
+ diag.info(" Move[\(wm.slotIdx)] wid=\(wm.wid): target=(\(Int(wm.target.origin.x)),\(Int(wm.target.origin.y))) \(Int(wm.target.width))x\(Int(wm.target.height)) sizeErr1=\(sizeErr1?.rawValue ?? -1) posErr=\(posErr?.rawValue ?? -1) sizeErr2=\(sizeErr2?.rawValue ?? -1)")
1886
+ resolvedAXElements.append((slotIdx: wm.slotIdx, el: axWin))
1814
1887
  moved += 1
1815
1888
  }
1889
+
1890
+ if !activatedPids.contains(pid) {
1891
+ if let app = NSRunningApplication(processIdentifier: pid) {
1892
+ app.activate()
1893
+ activatedPids.insert(pid)
1894
+ }
1895
+ }
1896
+
1897
+ AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, true as CFTypeRef)
1816
1898
  }
1817
1899
 
1818
- // Pass 2: Raise all windows in slot order so they all come to front
1819
- // Sort by slot index so the layout order is predictable
1900
+ if let cid { _ = _SLSReenableUpdate?(cid) }
1901
+
1902
+ // Pass 2: Raise all windows in slot order after app activation so final z-order matches the grid.
1820
1903
  resolvedAXElements.sort { $0.slotIdx < $1.slotIdx }
1821
1904
  for item in resolvedAXElements {
1822
1905
  AXUIElementPerformAction(item.el, kAXRaiseAction as CFString)
1906
+ AXUIElementSetAttributeValue(item.el, kAXMainAttribute as CFString, kCFBooleanTrue)
1823
1907
  }
1824
1908
  diag.info(" Raised \(resolvedAXElements.count) windows in slot order")
1825
1909
 
1826
- // Pass 3: Activate all apps so windows come to front of other apps
1827
- var activatedPids = Set<Int32>()
1828
- for win in windows {
1829
- if !activatedPids.contains(win.pid) {
1830
- if let app = NSRunningApplication(processIdentifier: win.pid) { app.activate() }
1831
- activatedPids.insert(win.pid)
1910
+ // Verify and retry drifted windows once using the battle-tested batch mover.
1911
+ let drifted = verifyMoves(moves)
1912
+ if !drifted.isEmpty {
1913
+ diag.warn(" Drifted after distribute: \(drifted.map(\.wid)) — retrying exact move path")
1914
+ usleep(100_000)
1915
+ batchMoveAndRaiseWindows(drifted)
1916
+ let stillDrifted = verifyMoves(drifted)
1917
+ if !stillDrifted.isEmpty {
1918
+ diag.warn(" Still drifted after retry: \(stillDrifted.map(\.wid))")
1832
1919
  }
1833
1920
  }
1834
1921
 
@@ -1837,8 +1924,10 @@ enum WindowTiler {
1837
1924
  }
1838
1925
  DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
1839
1926
  diag.success("batchRaiseAndDistribute: moved \(moved)/\(windows.count) [\(desc) grid]")
1840
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
1841
- NSApp.activate(ignoringOtherApps: true)
1927
+ if reactivateLattices {
1928
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
1929
+ NSApp.activate(ignoringOtherApps: true)
1930
+ }
1842
1931
  }
1843
1932
  }
1844
1933
 
@@ -1997,8 +2086,98 @@ enum WindowTiler {
1997
2086
  }) ?? NSScreen.main ?? primaryScreen
1998
2087
  }
1999
2088
 
2089
+ private static func currentMouseCGPoint() -> CGPoint {
2090
+ let mouseLocation = NSEvent.mouseLocation
2091
+ let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
2092
+ return CGPoint(x: mouseLocation.x, y: primaryHeight - mouseLocation.y)
2093
+ }
2094
+
2095
+ private static func switchToAdjacentSpaceViaSystemShortcut(offset: Int, displayId: String, initialSpaceId: Int) -> Int? {
2096
+ let keyCode: CGKeyCode = offset < 0 ? 123 : 124
2097
+ let script = """
2098
+ tell application "System Events"
2099
+ key code \(keyCode) using control down
2100
+ end tell
2101
+ return "ok"
2102
+ """
2103
+ let result = ProcessQuery.shell(["/usr/bin/osascript", "-e", script])
2104
+ if result != "ok" {
2105
+ DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut script did not complete for offset \(offset)")
2106
+ }
2107
+ return waitForSpaceChange(displayId: displayId, initialSpaceId: initialSpaceId, timeout: 1.2)
2108
+ }
2109
+
2110
+ private static func waitForSpaceChange(displayId: String, initialSpaceId: Int, timeout: TimeInterval) -> Int? {
2111
+ let deadline = Date().addingTimeInterval(timeout)
2112
+ while Date() < deadline {
2113
+ usleep(30_000)
2114
+ let current = getDisplaySpaces().first(where: { $0.displayId == displayId })?.currentSpaceId ?? 0
2115
+ if current != 0, current != initialSpaceId {
2116
+ return current
2117
+ }
2118
+ }
2119
+ return nil
2120
+ }
2121
+
2122
+ private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
2123
+ guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
2124
+ return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
2125
+ }
2126
+
2127
+ private static func resolvedCurrentSpaceId(for display: DisplaySpaces, activeSpaceId: Int) -> Int {
2128
+ if display.spaces.contains(where: { $0.id == activeSpaceId }) {
2129
+ return activeSpaceId
2130
+ }
2131
+ return display.currentSpaceId
2132
+ }
2133
+
2134
+ private static func formatCGPoint(_ point: CGPoint) -> String {
2135
+ "\(Int(point.x)),\(Int(point.y))"
2136
+ }
2137
+
2138
+ private static func screenIndex(for cgPoint: CGPoint) -> Int? {
2139
+ let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
2140
+ let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
2141
+ return NSScreen.screens.firstIndex(where: { $0.frame.contains(nsPoint) })
2142
+ ?? (NSScreen.main != nil ? 0 : nil)
2143
+ }
2144
+
2000
2145
  // MARK: - Private
2001
2146
 
2147
+ private static func visibleDistributableWindows() -> [WindowEntry] {
2148
+ DesktopModel.shared.allWindows().filter { entry in
2149
+ entry.isOnScreen &&
2150
+ entry.app != "Lattices" &&
2151
+ entry.frame.w > 50 &&
2152
+ entry.frame.h > 50
2153
+ }
2154
+ }
2155
+
2156
+ private static func sortWindowsForGrid(_ windows: [WindowEntry]) -> [WindowEntry] {
2157
+ windows.sorted { lhs, rhs in
2158
+ let rowTolerance = 40.0
2159
+ let yDelta = lhs.frame.y - rhs.frame.y
2160
+ if abs(yDelta) > rowTolerance {
2161
+ return lhs.frame.y < rhs.frame.y
2162
+ }
2163
+
2164
+ let xDelta = lhs.frame.x - rhs.frame.x
2165
+ if abs(xDelta) > rowTolerance {
2166
+ return lhs.frame.x < rhs.frame.x
2167
+ }
2168
+
2169
+ return lhs.zIndex < rhs.zIndex
2170
+ }
2171
+ }
2172
+
2173
+ private static func screenID(for screen: NSScreen) -> String {
2174
+ let key = NSDeviceDescriptionKey("NSScreenNumber")
2175
+ if let number = screen.deviceDescription[key] as? NSNumber {
2176
+ return number.stringValue
2177
+ }
2178
+ return screen.localizedName
2179
+ }
2180
+
2002
2181
  private static func tileAppleScript(app: String, tag: String, bounds: (Int, Int, Int, Int)) {
2003
2182
  let (x1, y1, x2, y2) = bounds
2004
2183
  let script = """