@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.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /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 =
|
|
54
|
+
func flash(frame: NSRect, duration: TimeInterval = 0.9) {
|
|
55
55
|
dismiss()
|
|
56
56
|
|
|
57
|
-
let inset: CGFloat = -
|
|
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 =
|
|
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 +
|
|
120
|
-
NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.
|
|
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.
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
654
|
-
wid =
|
|
655
|
-
diag.info("moveWindowToSpace:
|
|
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
|
|
728
|
-
diag.success("Path 1 (
|
|
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("
|
|
828
|
+
diag.warn("SessionWindowLocator failed — trying direct AX fallback")
|
|
734
829
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if let
|
|
740
|
-
|
|
741
|
-
|
|
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.
|
|
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 nil — no highlight")
|
|
755
842
|
}
|
|
756
843
|
diag.finish(t)
|
|
757
844
|
return
|
|
758
845
|
}
|
|
759
|
-
diag.warn("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1001
|
-
wid =
|
|
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
|
|
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(
|
|
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
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
-
|
|
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
|
|
1855
|
+
var axByWid: [UInt32: AXUIElement] = [:]
|
|
1773
1856
|
for axWin in axWindows {
|
|
1774
|
-
var
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
|
1807
|
-
AXUIElementSetAttributeValue(
|
|
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
|
|
1810
|
-
AXUIElementSetAttributeValue(
|
|
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=\(
|
|
1813
|
-
resolvedAXElements.append((slotIdx: slotIdx, 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
|
-
|
|
1819
|
-
|
|
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
|
-
//
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
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 = """
|