@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
@@ -0,0 +1,628 @@
1
+ import AppKit
2
+ import CoreGraphics
3
+
4
+ final class WindowDragSnapController {
5
+ static let shared = WindowDragSnapController()
6
+
7
+ private struct DragWindowCandidate {
8
+ let pid: pid_t
9
+ let wid: UInt32?
10
+ let axWindow: AXUIElement
11
+ let initialAXFrame: CGRect
12
+ }
13
+
14
+ private struct ResolvedSnapZone {
15
+ let id: String
16
+ let label: String
17
+ let placement: PlacementSpec
18
+ let screen: NSScreen
19
+ let screenID: String
20
+ let triggerRect: CGRect
21
+ let visibleRect: CGRect
22
+ let previewRect: CGRect
23
+ let priority: Int
24
+ }
25
+
26
+ private struct DragSession {
27
+ let pid: pid_t
28
+ let wid: UInt32?
29
+ let zones: [ResolvedSnapZone]
30
+ }
31
+
32
+ private var mouseDownMonitor: Any?
33
+ private var mouseDragMonitor: Any?
34
+ private var mouseUpMonitor: Any?
35
+ private var flagsChangedMonitor: Any?
36
+
37
+ private var dragCandidate: DragWindowCandidate?
38
+ private var activeSession: DragSession?
39
+ private var overlayPanels: [String: WindowSnapOverlayPanel] = [:]
40
+ private var modifierModeEnabled = false
41
+ private var windowHasMoved = false
42
+
43
+ private init() {}
44
+
45
+ func start() {
46
+ guard mouseDownMonitor == nil,
47
+ mouseDragMonitor == nil,
48
+ mouseUpMonitor == nil,
49
+ flagsChangedMonitor == nil else { return }
50
+
51
+ mouseDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
52
+ self?.handleMouseDown(event)
53
+ }
54
+ mouseDragMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDragged) { [weak self] event in
55
+ self?.handleMouseDragged(event)
56
+ }
57
+ mouseUpMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseUp) { [weak self] event in
58
+ self?.handleMouseUp(event)
59
+ }
60
+ flagsChangedMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
61
+ self?.handleFlagsChanged(event)
62
+ }
63
+
64
+ DiagnosticLog.shared.info("WindowDragSnap: global drag monitors started")
65
+ }
66
+
67
+ func stop() {
68
+ if let monitor = mouseDownMonitor { NSEvent.removeMonitor(monitor) }
69
+ if let monitor = mouseDragMonitor { NSEvent.removeMonitor(monitor) }
70
+ if let monitor = mouseUpMonitor { NSEvent.removeMonitor(monitor) }
71
+ if let monitor = flagsChangedMonitor { NSEvent.removeMonitor(monitor) }
72
+ mouseDownMonitor = nil
73
+ mouseDragMonitor = nil
74
+ mouseUpMonitor = nil
75
+ flagsChangedMonitor = nil
76
+ clearTracking()
77
+ }
78
+
79
+ private func handleMouseDown(_ event: NSEvent) {
80
+ guard Preferences.shared.dragSnapEnabled else {
81
+ clearTracking()
82
+ return
83
+ }
84
+ guard PermissionChecker.shared.accessibility else {
85
+ clearTracking()
86
+ return
87
+ }
88
+
89
+ WorkspaceManager.shared.loadGridConfig()
90
+ modifierModeEnabled = Self.snapModifierPressed()
91
+ windowHasMoved = false
92
+ activeSession = nil
93
+ hideOverlays()
94
+ dragCandidate = captureFocusedWindow(at: NSEvent.mouseLocation)
95
+ }
96
+
97
+ private func handleMouseDragged(_ event: NSEvent) {
98
+ guard Preferences.shared.dragSnapEnabled else {
99
+ clearTracking()
100
+ return
101
+ }
102
+ guard PermissionChecker.shared.accessibility else {
103
+ clearTracking()
104
+ return
105
+ }
106
+
107
+ guard let candidate = dragCandidate else { return }
108
+ modifierModeEnabled = Self.snapModifierPressed()
109
+ updateDragProgress(for: candidate)
110
+ updateSnapInteraction(at: NSEvent.mouseLocation)
111
+ }
112
+
113
+ private func handleFlagsChanged(_ event: NSEvent) {
114
+ guard dragCandidate != nil else { return }
115
+ modifierModeEnabled = Self.snapModifierPressed()
116
+ updateSnapInteraction(at: NSEvent.mouseLocation)
117
+ }
118
+
119
+ private func updateDragProgress(for candidate: DragWindowCandidate) {
120
+ guard let currentFrame = WindowTiler.readAXFrame(candidate.axWindow) else { return }
121
+
122
+ let moved = hypot(
123
+ currentFrame.origin.x - candidate.initialAXFrame.origin.x,
124
+ currentFrame.origin.y - candidate.initialAXFrame.origin.y
125
+ )
126
+ if moved >= 12 {
127
+ windowHasMoved = true
128
+ }
129
+ }
130
+
131
+ private func updateSnapInteraction(at mouseLocation: NSPoint) {
132
+ guard windowHasMoved else {
133
+ if activeSession != nil {
134
+ activeSession = nil
135
+ hideOverlays()
136
+ }
137
+ return
138
+ }
139
+
140
+ guard modifierModeEnabled else {
141
+ if activeSession != nil {
142
+ activeSession = nil
143
+ hideOverlays()
144
+ }
145
+ return
146
+ }
147
+
148
+ guard let candidate = dragCandidate else { return }
149
+ if activeSession == nil {
150
+ beginDragSession(with: candidate, mouseLocation: mouseLocation)
151
+ } else {
152
+ updateActiveSession(at: mouseLocation)
153
+ }
154
+ }
155
+
156
+ private func handleMouseUp(_ event: NSEvent) {
157
+ defer { clearTracking() }
158
+ modifierModeEnabled = Self.snapModifierPressed()
159
+ guard modifierModeEnabled, let activeSession else { return }
160
+
161
+ let mouseLocation = NSEvent.mouseLocation
162
+ guard let zone = bestZone(at: mouseLocation, in: activeSession.zones) else { return }
163
+
164
+ DiagnosticLog.shared.info("WindowDragSnap: drop → \(zone.label) (\(zone.id)) on \(zone.screen.localizedName)")
165
+ if let wid = activeSession.wid {
166
+ WindowTiler.tileWindowById(wid: wid, pid: activeSession.pid, to: zone.placement, on: zone.screen)
167
+ WindowTiler.highlightWindowById(wid: wid)
168
+ } else {
169
+ WindowTiler.tileFrontmostViaAX(to: zone.placement)
170
+ }
171
+ }
172
+
173
+ private func beginDragSession(with candidate: DragWindowCandidate, mouseLocation: NSPoint) {
174
+ WorkspaceManager.shared.loadGridConfig()
175
+ let config = WorkspaceManager.shared.snapZonesConfig
176
+ guard config.enabled ?? false else { return }
177
+
178
+ let zones = resolveZones(using: config)
179
+ guard !zones.isEmpty else { return }
180
+
181
+ activeSession = DragSession(
182
+ pid: candidate.pid,
183
+ wid: candidate.wid,
184
+ zones: zones
185
+ )
186
+
187
+ DiagnosticLog.shared.info("WindowDragSnap: tracking drag for pid=\(candidate.pid) wid=\(candidate.wid ?? 0)")
188
+ updateActiveSession(at: mouseLocation)
189
+ }
190
+
191
+ private func updateActiveSession(at mouseLocation: NSPoint) {
192
+ guard let activeSession else { return }
193
+ let hoveredZone = bestZone(at: mouseLocation, in: activeSession.zones)
194
+ render(zones: activeSession.zones, hoveredZone: hoveredZone)
195
+ }
196
+
197
+ private func clearTracking() {
198
+ dragCandidate = nil
199
+ activeSession = nil
200
+ modifierModeEnabled = false
201
+ windowHasMoved = false
202
+ hideOverlays()
203
+ }
204
+
205
+ private func hideOverlays() {
206
+ for panel in overlayPanels.values {
207
+ panel.orderOut(nil)
208
+ }
209
+ }
210
+
211
+ private func captureFocusedWindow(at mouseLocation: NSPoint) -> DragWindowCandidate? {
212
+ guard let frontApp = NSWorkspace.shared.frontmostApplication,
213
+ frontApp.bundleIdentifier != Bundle.main.bundleIdentifier else {
214
+ return nil
215
+ }
216
+
217
+ let appRef = AXUIElementCreateApplication(frontApp.processIdentifier)
218
+ var focusedRef: CFTypeRef?
219
+ guard AXUIElementCopyAttributeValue(appRef, kAXFocusedWindowAttribute as CFString, &focusedRef) == .success,
220
+ let focusedRef else {
221
+ return nil
222
+ }
223
+ let axWindow = focusedRef as! AXUIElement
224
+ guard let axFrame = WindowTiler.readAXFrame(axWindow) else { return nil }
225
+
226
+ let windowRect = Self.screenRect(fromAX: axFrame)
227
+ guard windowRect.insetBy(dx: -8, dy: -8).contains(mouseLocation) else {
228
+ return nil
229
+ }
230
+
231
+ var widValue: CGWindowID = 0
232
+ let wid = _AXUIElementGetWindow(axWindow, &widValue) == .success ? widValue : nil
233
+
234
+ return DragWindowCandidate(
235
+ pid: frontApp.processIdentifier,
236
+ wid: wid,
237
+ axWindow: axWindow,
238
+ initialAXFrame: axFrame
239
+ )
240
+ }
241
+
242
+ private func resolveZones(using config: SnapZonesConfig) -> [ResolvedSnapZone] {
243
+ let wm = WorkspaceManager.shared
244
+ let baseZones = (config.rules ?? []).compactMap { zone -> (SnapZoneDefinition, PlacementSpec, (CGFloat, CGFloat, CGFloat, CGFloat), Int)? in
245
+ let placement: PlacementSpec
246
+ switch zone.placement {
247
+ case .named(let name):
248
+ guard let resolved = wm.resolvePlacement(name) else {
249
+ DiagnosticLog.shared.warn("WindowDragSnap: ignoring snap zone \(zone.id) — unknown placement \(name)")
250
+ return nil
251
+ }
252
+ placement = resolved
253
+ case .fractions(let fractionalPlacement):
254
+ placement = .fractions(fractionalPlacement)
255
+ }
256
+
257
+ let triggerFractions: (CGFloat, CGFloat, CGFloat, CGFloat)
258
+ switch zone.trigger {
259
+ case .named(let name):
260
+ guard let triggerPlacement = wm.resolvePlacement(name) else {
261
+ DiagnosticLog.shared.warn("WindowDragSnap: ignoring snap zone \(zone.id) — unknown trigger \(name)")
262
+ return nil
263
+ }
264
+ triggerFractions = triggerPlacement.fractions
265
+ case .fractions(let placement):
266
+ triggerFractions = placement.fractions
267
+ }
268
+
269
+ return (zone, placement, triggerFractions, zone.priority ?? 0)
270
+ }
271
+
272
+ var resolved: [ResolvedSnapZone] = []
273
+ for screen in NSScreen.screens {
274
+ let screenID = Self.screenID(for: screen)
275
+ for (zone, placement, triggerFractions, priority) in baseZones {
276
+ let triggerRect = Self.screenRect(for: triggerFractions, on: screen)
277
+ let previewRect = Self.screenRect(fromAX: WindowTiler.tileFrame(for: placement, on: screen))
278
+ let visibleRect = Self.visibleRect(forTriggerRect: triggerRect, previewRect: previewRect, on: screen)
279
+ resolved.append(
280
+ ResolvedSnapZone(
281
+ id: zone.id,
282
+ label: zone.label ?? zone.id,
283
+ placement: placement,
284
+ screen: screen,
285
+ screenID: screenID,
286
+ triggerRect: triggerRect,
287
+ visibleRect: visibleRect,
288
+ previewRect: previewRect,
289
+ priority: priority
290
+ )
291
+ )
292
+ }
293
+ }
294
+
295
+ return resolved.sorted {
296
+ if $0.priority != $1.priority {
297
+ return $0.priority > $1.priority
298
+ }
299
+ let leftArea = $0.triggerRect.width * $0.triggerRect.height
300
+ let rightArea = $1.triggerRect.width * $1.triggerRect.height
301
+ if leftArea != rightArea {
302
+ return leftArea < rightArea
303
+ }
304
+ return $0.id < $1.id
305
+ }
306
+ }
307
+
308
+ private func bestZone(at mouseLocation: NSPoint, in zones: [ResolvedSnapZone]) -> ResolvedSnapZone? {
309
+ zones.first(where: { $0.triggerRect.contains(mouseLocation) })
310
+ }
311
+
312
+ private func render(zones: [ResolvedSnapZone], hoveredZone: ResolvedSnapZone?) {
313
+ let config = WorkspaceManager.shared.snapZonesConfig
314
+ let grouped = Dictionary(grouping: zones, by: \.screenID)
315
+ let activeScreenIDs = Set(grouped.keys)
316
+
317
+ for screen in NSScreen.screens {
318
+ let screenID = Self.screenID(for: screen)
319
+ guard let screenZones = grouped[screenID], !screenZones.isEmpty else { continue }
320
+
321
+ let panel = overlayPanels[screenID] ?? makeOverlayPanel(for: screen)
322
+ panel.setFrame(screen.frame, display: false)
323
+
324
+ let localZones = screenZones.map {
325
+ WindowSnapOverlayView.Zone(
326
+ id: $0.id,
327
+ label: $0.label,
328
+ rect: $0.visibleRect.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY),
329
+ isHovered: hoveredZone?.id == $0.id && hoveredZone?.screenID == screenID
330
+ )
331
+ }
332
+
333
+ let previewRect = hoveredZone?.screenID == screenID
334
+ ? hoveredZone?.previewRect.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY)
335
+ : nil
336
+
337
+ panel.overlayView.model = WindowSnapOverlayView.Model(
338
+ zones: localZones,
339
+ previewRect: previewRect,
340
+ previewLabel: nil,
341
+ zoneOpacity: CGFloat(config.zoneOpacity ?? SnapZonesConfig.defaults.zoneOpacity ?? 0.10),
342
+ highlightOpacity: CGFloat(config.highlightOpacity ?? SnapZonesConfig.defaults.highlightOpacity ?? 0.22),
343
+ previewOpacity: CGFloat(config.previewOpacity ?? SnapZonesConfig.defaults.previewOpacity ?? 0.18),
344
+ cornerRadius: config.cornerRadius ?? SnapZonesConfig.defaults.cornerRadius ?? 18
345
+ )
346
+
347
+ panel.orderFrontRegardless()
348
+ }
349
+
350
+ for (screenID, panel) in overlayPanels where !activeScreenIDs.contains(screenID) {
351
+ panel.orderOut(nil)
352
+ }
353
+ }
354
+
355
+ private func makeOverlayPanel(for screen: NSScreen) -> WindowSnapOverlayPanel {
356
+ let panel = WindowSnapOverlayPanel(frame: screen.frame)
357
+ overlayPanels[Self.screenID(for: screen)] = panel
358
+ return panel
359
+ }
360
+
361
+ private static func screenID(for screen: NSScreen) -> String {
362
+ let key = NSDeviceDescriptionKey("NSScreenNumber")
363
+ if let number = screen.deviceDescription[key] as? NSNumber {
364
+ return number.stringValue
365
+ }
366
+ return screen.localizedName
367
+ }
368
+
369
+ private static func screenRect(for fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on screen: NSScreen) -> CGRect {
370
+ let visible = screen.visibleFrame
371
+ let (fx, fy, fw, fh) = fractions
372
+ return CGRect(
373
+ x: visible.minX + visible.width * fx,
374
+ y: visible.maxY - visible.height * (fy + fh),
375
+ width: visible.width * fw,
376
+ height: visible.height * fh
377
+ )
378
+ }
379
+
380
+ private static func screenRect(fromAX rect: CGRect) -> CGRect {
381
+ let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
382
+ return CGRect(
383
+ x: rect.origin.x,
384
+ y: primaryHeight - rect.origin.y - rect.height,
385
+ width: rect.width,
386
+ height: rect.height
387
+ )
388
+ }
389
+
390
+ private static func snapModifierPressed() -> Bool {
391
+ let flags = CGEventSource.flagsState(.combinedSessionState)
392
+ return flags.contains(Self.snapModifier().cgEventFlags)
393
+ }
394
+
395
+ private static func snapModifier() -> SnapModifierKey {
396
+ WorkspaceManager.shared.snapZonesConfig.modifier ?? .command
397
+ }
398
+
399
+ private static func visibleRect(forTriggerRect triggerRect: CGRect, previewRect: CGRect, on screen: NSScreen) -> CGRect {
400
+ let visible = screen.visibleFrame
401
+ let inset: CGFloat = 18
402
+ let nearLeft = abs(triggerRect.minX - visible.minX) < 8
403
+ let nearRight = abs(triggerRect.maxX - visible.maxX) < 8
404
+ let nearTop = abs(triggerRect.maxY - visible.maxY) < 8
405
+ let nearBottom = abs(triggerRect.minY - visible.minY) < 8
406
+
407
+ if (nearLeft || nearRight) && (nearTop || nearBottom) {
408
+ let width: CGFloat = 94
409
+ let height: CGFloat = 56
410
+ let x = nearLeft ? visible.minX + inset : visible.maxX - inset - width
411
+ let y = nearBottom ? visible.minY + inset : visible.maxY - inset - height
412
+ return CGRect(x: x, y: y, width: width, height: height)
413
+ }
414
+
415
+ if nearLeft || nearRight {
416
+ let width: CGFloat = 110
417
+ let height: CGFloat = 38
418
+ let x = nearLeft ? visible.minX + inset : visible.maxX - inset - width
419
+ let y = clamp(previewRect.midY - height / 2, min: visible.minY + 54, max: visible.maxY - 54 - height)
420
+ return CGRect(x: x, y: y, width: width, height: height)
421
+ }
422
+
423
+ if nearTop || nearBottom {
424
+ let width = min(max(triggerRect.width * 0.34, 132), 240)
425
+ let height: CGFloat = 38
426
+ let x = clamp(previewRect.midX - width / 2, min: visible.minX + 54, max: visible.maxX - 54 - width)
427
+ let y = nearBottom ? visible.minY + inset : visible.maxY - inset - height
428
+ return CGRect(x: x, y: y, width: width, height: height)
429
+ }
430
+
431
+ let width = min(max(previewRect.width * 0.28, 132), 220)
432
+ let height: CGFloat = 38
433
+ let x = clamp(previewRect.midX - width / 2, min: visible.minX + 54, max: visible.maxX - 54 - width)
434
+ let y = clamp(previewRect.maxY - height - 16, min: visible.minY + 40, max: visible.maxY - 40 - height)
435
+ return CGRect(x: x, y: y, width: width, height: height)
436
+ }
437
+
438
+ private static func clamp(_ value: CGFloat, min lower: CGFloat, max upper: CGFloat) -> CGFloat {
439
+ Swift.max(lower, Swift.min(upper, value))
440
+ }
441
+ }
442
+
443
+ private final class WindowSnapOverlayPanel: NSPanel {
444
+ let overlayView = WindowSnapOverlayView(frame: .zero)
445
+
446
+ init(frame: CGRect) {
447
+ super.init(
448
+ contentRect: frame,
449
+ styleMask: [.borderless, .nonactivatingPanel],
450
+ backing: .buffered,
451
+ defer: false
452
+ )
453
+
454
+ isOpaque = false
455
+ backgroundColor = .clear
456
+ hasShadow = false
457
+ ignoresMouseEvents = true
458
+ level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
459
+ collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
460
+ isMovable = false
461
+ hidesOnDeactivate = false
462
+ animationBehavior = .none
463
+ overlayView.frame = NSRect(origin: .zero, size: frame.size)
464
+ overlayView.autoresizingMask = [.width, .height]
465
+ contentView = overlayView
466
+ }
467
+
468
+ override var canBecomeKey: Bool { false }
469
+ override var canBecomeMain: Bool { false }
470
+ }
471
+
472
+ private final class WindowSnapOverlayView: NSView {
473
+ struct Zone {
474
+ let id: String
475
+ let label: String
476
+ let rect: CGRect
477
+ let isHovered: Bool
478
+ }
479
+
480
+ struct Model {
481
+ let zones: [Zone]
482
+ let previewRect: CGRect?
483
+ let previewLabel: String?
484
+ let zoneOpacity: CGFloat
485
+ let highlightOpacity: CGFloat
486
+ let previewOpacity: CGFloat
487
+ let cornerRadius: CGFloat
488
+
489
+ static let empty = Model(
490
+ zones: [],
491
+ previewRect: nil,
492
+ previewLabel: nil,
493
+ zoneOpacity: 0.10,
494
+ highlightOpacity: 0.22,
495
+ previewOpacity: 0.18,
496
+ cornerRadius: 18
497
+ )
498
+ }
499
+
500
+ var model: Model = .empty {
501
+ didSet { needsDisplay = true }
502
+ }
503
+
504
+ override init(frame frameRect: NSRect) {
505
+ super.init(frame: frameRect)
506
+ wantsLayer = true
507
+ layer?.backgroundColor = NSColor.clear.cgColor
508
+ }
509
+
510
+ required init?(coder: NSCoder) {
511
+ fatalError("init(coder:) has not been implemented")
512
+ }
513
+
514
+ override func draw(_ dirtyRect: NSRect) {
515
+ NSColor.clear.setFill()
516
+ bounds.fill()
517
+
518
+ for zone in model.zones {
519
+ drawZone(zone)
520
+ }
521
+
522
+ if let previewRect = model.previewRect {
523
+ drawPreview(previewRect, label: model.previewLabel)
524
+ }
525
+ }
526
+
527
+ private func drawZone(_ zone: Zone) {
528
+ let rect = zone.rect.insetBy(dx: 1.5, dy: 1.5)
529
+ let radius = min(model.cornerRadius, min(rect.width, rect.height) * 0.34)
530
+ let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
531
+ let idleStrength = max(0.35, min(model.zoneOpacity / 0.10, 1.4))
532
+ let hoverStrength = max(0.35, min(model.highlightOpacity / 0.22, 1.4))
533
+
534
+ let shadow = NSShadow()
535
+ shadow.shadowBlurRadius = zone.isHovered ? 18 : 10
536
+ shadow.shadowOffset = NSSize(width: 0, height: -2)
537
+ shadow.shadowColor = NSColor.black.withAlphaComponent(zone.isHovered ? 0.20 : 0.10)
538
+
539
+ NSGraphicsContext.saveGraphicsState()
540
+ shadow.set()
541
+ let baseTop = NSColor(
542
+ calibratedWhite: 0.13,
543
+ alpha: zone.isHovered ? 0.42 * hoverStrength : 0.22 * idleStrength
544
+ )
545
+ let baseBottom = NSColor(
546
+ calibratedWhite: 0.07,
547
+ alpha: zone.isHovered ? 0.34 * hoverStrength : 0.15 * idleStrength
548
+ )
549
+ NSGradient(starting: baseTop, ending: baseBottom)?.draw(in: path, angle: -90)
550
+ NSGraphicsContext.restoreGraphicsState()
551
+
552
+ if zone.isHovered {
553
+ let glowPath = path.copy() as! NSBezierPath
554
+ glowPath.lineWidth = 6
555
+ NSColor(calibratedRed: 0.25, green: 0.84, blue: 0.58, alpha: model.highlightOpacity * 0.28).setStroke()
556
+ glowPath.stroke()
557
+ }
558
+
559
+ path.lineWidth = zone.isHovered ? 1.6 : 1.0
560
+ NSColor(
561
+ calibratedRed: 0.52,
562
+ green: 0.94,
563
+ blue: 0.72,
564
+ alpha: zone.isHovered ? 0.54 * hoverStrength : 0.10 * idleStrength
565
+ ).setStroke()
566
+ path.stroke()
567
+
568
+ let lipRect = CGRect(x: rect.minX + 1.5, y: rect.maxY - 2.5, width: rect.width - 3, height: 2)
569
+ if lipRect.width > 0 {
570
+ let lipPath = NSBezierPath(roundedRect: lipRect, xRadius: 1, yRadius: 1)
571
+ NSColor.white.withAlphaComponent(zone.isHovered ? 0.18 : 0.08).setFill()
572
+ lipPath.fill()
573
+ }
574
+
575
+ drawLabel(zone.label, in: rect, emphasized: zone.isHovered)
576
+ }
577
+
578
+ private func drawPreview(_ rect: CGRect, label: String?) {
579
+ let previewRect = rect.insetBy(dx: 10, dy: 10)
580
+ let radius = min(model.cornerRadius, min(previewRect.width, previewRect.height) * 0.14)
581
+ let path = NSBezierPath(roundedRect: previewRect, xRadius: radius, yRadius: radius)
582
+
583
+ NSColor(calibratedWhite: 1.0, alpha: model.previewOpacity * 0.22).setFill()
584
+ path.fill()
585
+
586
+ path.lineWidth = 1.6
587
+ path.setLineDash([10, 8], count: 2, phase: 0)
588
+ NSColor(
589
+ calibratedRed: 0.44,
590
+ green: 0.90,
591
+ blue: 0.68,
592
+ alpha: max(0.34, model.previewOpacity * 3.2)
593
+ ).setStroke()
594
+ path.stroke()
595
+ path.setLineDash([], count: 0, phase: 0)
596
+
597
+ let innerPath = NSBezierPath(roundedRect: previewRect.insetBy(dx: 7, dy: 7), xRadius: max(radius - 4, 8), yRadius: max(radius - 4, 8))
598
+ innerPath.lineWidth = 1
599
+ NSColor.white.withAlphaComponent(max(0.08, model.previewOpacity * 1.2)).setStroke()
600
+ innerPath.stroke()
601
+
602
+ if let label {
603
+ let tagRect = CGRect(x: previewRect.minX + 14, y: previewRect.maxY - 34, width: 110, height: 24)
604
+ let tagPath = NSBezierPath(roundedRect: tagRect, xRadius: 12, yRadius: 12)
605
+ NSColor(calibratedWhite: 0.08, alpha: 0.62).setFill()
606
+ tagPath.fill()
607
+ NSColor.white.withAlphaComponent(0.10).setStroke()
608
+ tagPath.lineWidth = 1
609
+ tagPath.stroke()
610
+ drawLabel(label, in: tagRect, emphasized: true)
611
+ }
612
+ }
613
+
614
+ private func drawLabel(_ label: String, in rect: CGRect, emphasized: Bool) {
615
+ let font = NSFont.monospacedSystemFont(ofSize: emphasized ? 11 : 10, weight: emphasized ? .semibold : .medium)
616
+ let attributes: [NSAttributedString.Key: Any] = [
617
+ .font: font,
618
+ .foregroundColor: NSColor.white.withAlphaComponent(emphasized ? 0.92 : 0.72),
619
+ ]
620
+ let attr = NSAttributedString(string: label.uppercased(), attributes: attributes)
621
+ let size = attr.size()
622
+ let drawPoint = CGPoint(
623
+ x: rect.midX - size.width / 2,
624
+ y: rect.midY - size.height / 2
625
+ )
626
+ attr.draw(at: drawPoint)
627
+ }
628
+ }