@lattices/cli 0.4.13 → 0.5.0

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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +191 -63
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2271
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,1566 +0,0 @@
1
- import AppKit
2
- import Foundation
3
-
4
- // MARK: - Phase
5
-
6
- enum CommandModePhase: Equatable {
7
- case idle
8
- case inventory
9
- case desktopInventory
10
- case executing(String)
11
- }
12
-
13
- // MARK: - Inventory Snapshot
14
-
15
- struct CommandModeInventory {
16
- struct Item {
17
- let name: String
18
- let group: String // "Layer: X", "Group: Y", "Orphan"
19
- let status: Status
20
- let paneCount: Int
21
- let tileHint: String? // "left", "right", etc.
22
- }
23
- enum Status { case running, attached, stopped }
24
-
25
- let activeLayer: String?
26
- let layerCount: Int
27
- let items: [Item]
28
- }
29
-
30
- // MARK: - Chord
31
-
32
- struct Chord {
33
- let key: String // display label e.g. "a", "1"
34
- let keyCode: UInt16
35
- let label: String // e.g. "tile all"
36
- let action: () -> Void
37
- }
38
-
39
- // MARK: - Desktop Inventory Mode
40
-
41
- enum DesktopInventoryMode: Equatable {
42
- case browsing
43
- case tiling // t → tile picker
44
- case gridPreview // s → preview grid layout before applying
45
- case screenMap // m → interactive screen map editor
46
- }
47
-
48
- enum CommandModeLaunchMode: Equatable {
49
- case normal
50
- case organize(appName: String?)
51
- }
52
-
53
- // DisplayGeometry, ScreenMapWindowEntry, ScreenMapEditorState, ScreenMapActionLog
54
- // are defined in ScreenMapState.swift
55
- // MARK: - Filter Presets
56
-
57
- enum FilterPreset: String, CaseIterable {
58
- case all = "All"
59
- case terminals = "Terminals"
60
- case editors = "Editors"
61
- case browsers = "Browsers"
62
- case lattices = "Lattices"
63
- case currentSpace = "Current Space"
64
-
65
- var appTypes: Set<AppType>? {
66
- switch self {
67
- case .all: return nil
68
- case .terminals: return [.terminal]
69
- case .editors: return [.editor]
70
- case .browsers: return [.browser]
71
- case .lattices: return nil // special case
72
- case .currentSpace: return nil // special case
73
- }
74
- }
75
-
76
- var keyIndex: Int? {
77
- switch self {
78
- case .all: return 1
79
- case .terminals: return 2
80
- case .editors: return 3
81
- case .browsers: return 4
82
- case .lattices: return 5
83
- case .currentSpace: return 6
84
- }
85
- }
86
-
87
- static func from(keyIndex: Int) -> FilterPreset? {
88
- allCases.first { $0.keyIndex == keyIndex }
89
- }
90
- }
91
-
92
- // MARK: - State Machine
93
-
94
- final class CommandModeState: ObservableObject {
95
- @Published var phase: CommandModePhase = .idle
96
- @Published var inventory = CommandModeInventory(activeLayer: nil, layerCount: 0, items: [])
97
- @Published var chords: [Chord] = []
98
- @Published var desktopSnapshot: DesktopInventorySnapshot?
99
- @Published var selectedWindowIds: Set<UInt32> = [] {
100
- didSet { syncSharedSelection() }
101
- }
102
- @Published var desktopMode: DesktopInventoryMode = .browsing
103
- @Published var activePreset: FilterPreset? = nil
104
- @Published var searchQuery: String = ""
105
- @Published var isSearching: Bool = false
106
- @Published var gridPreviewPlacement: PlacementSpec? = nil
107
-
108
- // MARK: - Marquee Drag State
109
- @Published var isDragging: Bool = false
110
- @Published var marqueeOrigin: CGPoint = .zero
111
- @Published var marqueeCurrentPoint: CGPoint = .zero
112
-
113
- /// Computed normalized rect from origin → current drag point
114
- var marqueeRect: CGRect {
115
- let x = min(marqueeOrigin.x, marqueeCurrentPoint.x)
116
- let y = min(marqueeOrigin.y, marqueeCurrentPoint.y)
117
- let w = abs(marqueeCurrentPoint.x - marqueeOrigin.x)
118
- let h = abs(marqueeCurrentPoint.y - marqueeOrigin.y)
119
- return CGRect(x: x, y: y, width: w, height: h)
120
- }
121
-
122
- /// Row frames in inventoryPanel coordinate space (updated by PreferenceKey)
123
- var rowFrames: [UInt32: CGRect] = [:]
124
-
125
- /// Raw mouse-down point for drag threshold detection (screen coordinates)
126
- var dragStartPoint: NSPoint?
127
-
128
- /// Selection state before drag started (for Cmd+drag additive mode)
129
- private var preDragSelection: Set<UInt32> = []
130
-
131
- // MARK: - Saved Positions (for restore after show & distribute)
132
- /// Saved window frames before a show/distribute action — allows undo
133
- @Published var savedPositions: [UInt32: (pid: Int32, frame: WindowFrame)]? = nil
134
-
135
- /// Brief flash message shown after an action (auto-dismisses)
136
- @Published var flashMessage: String? = nil
137
-
138
- var onDismiss: (() -> Void)?
139
- var onPanelResize: ((_ width: CGFloat, _ height: CGFloat) -> Void)?
140
- private let launchMode: CommandModeLaunchMode
141
-
142
- /// Tracks the last item navigated to, for consistent Shift+arrow multi-select
143
- private var cursorWindowId: UInt32?
144
-
145
- init(launchMode: CommandModeLaunchMode = .normal) {
146
- self.launchMode = launchMode
147
- }
148
-
149
- var isOrganizeFlow: Bool {
150
- if case .organize = launchMode { return true }
151
- return false
152
- }
153
-
154
- var organizeSeedAppName: String? {
155
- if case .organize(let appName) = launchMode { return appName }
156
- return nil
157
- }
158
-
159
- var organizeSelectionSummary: String {
160
- let count = selectedWindowIds.count
161
- if let appName = organizeSeedAppName, !appName.isEmpty {
162
- return "\(count) \(appName) window\(count == 1 ? "" : "s") selected"
163
- }
164
- return "\(count) window\(count == 1 ? "" : "s") selected"
165
- }
166
-
167
- var organizeGuidance: String {
168
- if selectedWindowIds.count > 1 {
169
- return "Press D to organize. Cmd-click adds or removes windows. Shift-click extends the selection."
170
- }
171
- if selectedWindowIds.count == 1 {
172
- return "Cmd-click another window to add it, then press D to organize the set."
173
- }
174
- return "Click windows to select them. Cmd-click adds or removes windows, and D organizes the selection."
175
- }
176
-
177
- // MARK: - Selection Helpers
178
-
179
- /// Backwards-compat: returns single selected ID (first element)
180
- var selectedWindowId: UInt32? {
181
- selectedWindowIds.first
182
- }
183
-
184
- var selectedWindowSummaryText: String {
185
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
186
- let labels = windows.compactMap { $0.appName }.uniquePrefix(3)
187
- guard !labels.isEmpty else { return "" }
188
- if windows.count > labels.count {
189
- return labels.joined(separator: " • ") + " +\(windows.count - labels.count)"
190
- }
191
- return labels.joined(separator: " • ")
192
- }
193
-
194
- var gridPreviewRegionLabel: String {
195
- guard let placement = gridPreviewPlacement else { return "Full Screen" }
196
- switch placement {
197
- case .tile(let position):
198
- return position.label
199
- default:
200
- return placement.wireValue
201
- }
202
- }
203
-
204
- func isSelected(_ id: UInt32) -> Bool {
205
- selectedWindowIds.contains(id)
206
- }
207
-
208
- func selectSingle(_ id: UInt32) {
209
- selectedWindowIds = [id]
210
- cursorWindowId = id
211
- }
212
-
213
- func toggleSelection(_ id: UInt32) {
214
- if selectedWindowIds.contains(id) {
215
- selectedWindowIds.remove(id)
216
- } else {
217
- selectedWindowIds.insert(id)
218
- }
219
- cursorWindowId = id
220
- }
221
-
222
- func clearSelection() {
223
- selectedWindowIds = []
224
- cursorWindowId = nil
225
- isDragging = false
226
- dragStartPoint = nil
227
- }
228
-
229
- /// Select contiguous range from cursor anchor to target (Shift+click)
230
- func selectRange(to targetId: UInt32) {
231
- guard let anchorId = cursorWindowId else { selectSingle(targetId); return }
232
- let list = flatWindowList
233
- guard let anchorIdx = list.firstIndex(where: { $0.id == anchorId }),
234
- let targetIdx = list.firstIndex(where: { $0.id == targetId }) else {
235
- selectSingle(targetId)
236
- return
237
- }
238
- let lo = min(anchorIdx, targetIdx)
239
- let hi = max(anchorIdx, targetIdx)
240
- selectedWindowIds = Set(list[lo...hi].map(\.id))
241
- // cursorWindowId stays as anchor for subsequent Shift+clicks
242
- }
243
-
244
- // MARK: - Marquee Drag
245
-
246
- func beginDrag(at point: CGPoint, additive: Bool) {
247
- preDragSelection = additive ? selectedWindowIds : []
248
- marqueeOrigin = point
249
- marqueeCurrentPoint = point
250
- isDragging = true
251
- }
252
-
253
- func updateDrag(to point: CGPoint) {
254
- marqueeCurrentPoint = point
255
- updateMarqueeSelection()
256
- }
257
-
258
- func endDrag() {
259
- isDragging = false
260
- dragStartPoint = nil
261
- preDragSelection = []
262
- }
263
-
264
- /// Select all rows whose frames intersect the current marquee rect
265
- private func updateMarqueeSelection() {
266
- let rect = marqueeRect
267
- var hits = preDragSelection
268
- for (wid, frame) in rowFrames {
269
- if rect.intersects(frame) {
270
- hits.insert(wid)
271
- }
272
- }
273
- selectedWindowIds = hits
274
- if let first = hits.first { cursorWindowId = first }
275
- }
276
-
277
- func activateSearch() {
278
- isSearching = true
279
- searchQuery = ""
280
- clearSelection()
281
- }
282
-
283
- func deactivateSearch() {
284
- isSearching = false
285
- searchQuery = ""
286
- }
287
-
288
- /// Filtered desktop snapshot based on active preset and search query
289
- var filteredSnapshot: DesktopInventorySnapshot? {
290
- guard let snapshot = desktopSnapshot else { return nil }
291
-
292
- let needsPresetFilter = activePreset != nil && activePreset != .all
293
- let needsSearchFilter = isSearching && !searchQuery.isEmpty
294
- guard needsPresetFilter || needsSearchFilter else { return snapshot }
295
-
296
- let query = searchQuery.lowercased()
297
-
298
- let filteredDisplays = snapshot.displays.compactMap { display -> DesktopInventorySnapshot.DisplayInfo? in
299
- let filteredSpaces = display.spaces.compactMap { space -> DesktopInventorySnapshot.SpaceGroup? in
300
- if let preset = activePreset, preset == .currentSpace && !space.isCurrent { return nil }
301
-
302
- let filteredApps = space.apps.compactMap { appGroup -> DesktopInventorySnapshot.AppGroup? in
303
- let filteredWindows = appGroup.windows.filter { win in
304
- // Preset filter
305
- if let preset = activePreset, preset != .all {
306
- let passesPreset: Bool
307
- switch preset {
308
- case .lattices: passesPreset = win.isLattices
309
- case .currentSpace: passesPreset = true
310
- default:
311
- if let types = preset.appTypes, let name = win.appName {
312
- passesPreset = types.contains(AppTypeClassifier.classify(name))
313
- } else {
314
- passesPreset = false
315
- }
316
- }
317
- if !passesPreset { return false }
318
- }
319
-
320
- // Search filter
321
- if needsSearchFilter {
322
- let matchesApp = win.appName?.lowercased().contains(query) ?? false
323
- let matchesTitle = win.title.lowercased().contains(query)
324
- let matchesLattices = win.latticesSession?.lowercased().contains(query) ?? false
325
- let matchesOcr = OcrModel.shared.results[win.id]?.fullText
326
- .lowercased().contains(query) ?? false
327
- if !matchesApp && !matchesTitle && !matchesLattices && !matchesOcr { return false }
328
- }
329
-
330
- return true
331
- }
332
- guard !filteredWindows.isEmpty else { return nil }
333
- return DesktopInventorySnapshot.AppGroup(
334
- id: appGroup.id, appName: appGroup.appName, windows: filteredWindows
335
- )
336
- }
337
- guard !filteredApps.isEmpty else { return nil }
338
- return DesktopInventorySnapshot.SpaceGroup(
339
- id: space.id, index: space.index, isCurrent: space.isCurrent, apps: filteredApps
340
- )
341
- }
342
- guard !filteredSpaces.isEmpty else { return nil }
343
- return DesktopInventorySnapshot.DisplayInfo(
344
- id: display.id, name: display.name, resolution: display.resolution,
345
- visibleFrame: display.visibleFrame, isMain: display.isMain,
346
- spaceCount: display.spaceCount, currentSpaceIndex: display.currentSpaceIndex,
347
- spaces: filteredSpaces
348
- )
349
- }
350
- return DesktopInventorySnapshot(displays: filteredDisplays, timestamp: snapshot.timestamp)
351
- }
352
-
353
- /// Compact panel size for chord view
354
- private let chordPanelSize: (CGFloat, CGFloat) = (580, 360)
355
-
356
- /// Compute desktop inventory panel size based on display count, clamped to screen
357
- private var desktopPanelSize: (CGFloat, CGFloat) {
358
- let displayCount = max(1, desktopSnapshot?.displays.count ?? 1)
359
- let ideal = CGFloat(displayCount) * 480 + CGFloat(displayCount - 1) + 32
360
- let screenWidth = NSScreen.main?.visibleFrame.width ?? 1920
361
- let width = min(ideal, screenWidth * 0.92)
362
- let height: CGFloat = 640
363
- return (width, height)
364
- }
365
-
366
- /// Flat window list for keyboard navigation (respects active filter)
367
- var flatWindowList: [DesktopInventorySnapshot.InventoryWindowInfo] {
368
- filteredSnapshot?.allWindows ?? []
369
- }
370
-
371
- var ocrMatchSnippets: [UInt32: String] {
372
- guard isSearching, !searchQuery.isEmpty else { return [:] }
373
- let query = searchQuery.lowercased()
374
- let ocrResults = OcrModel.shared.results
375
- var snippets: [UInt32: String] = [:]
376
- for win in flatWindowList {
377
- // Only show snippet if match came from OCR, not title/app
378
- let matchesApp = win.appName?.lowercased().contains(query) ?? false
379
- let matchesTitle = win.title.lowercased().contains(query)
380
- let matchesLattices = win.latticesSession?.lowercased().contains(query) ?? false
381
- if matchesApp || matchesTitle || matchesLattices { continue }
382
- if let ocr = ocrResults[win.id],
383
- let range = ocr.fullText.lowercased().range(of: query) {
384
- snippets[win.id] = Self.extractSnippet(from: ocr.fullText, around: range)
385
- }
386
- }
387
- return snippets
388
- }
389
-
390
- private static func extractSnippet(from text: String, around range: Range<String.Index>, maxLen: Int = 80) -> String {
391
- let half = max(0, (maxLen - text.distance(from: range.lowerBound, to: range.upperBound)) / 2)
392
- let start = text.index(range.lowerBound, offsetBy: -half, limitedBy: text.startIndex) ?? text.startIndex
393
- let end = text.index(range.upperBound, offsetBy: half, limitedBy: text.endIndex) ?? text.endIndex
394
- var s = String(text[start..<end])
395
- .replacingOccurrences(of: "\n", with: " ")
396
- .trimmingCharacters(in: .whitespaces)
397
- if start > text.startIndex { s = "…" + s }
398
- if end < text.endIndex { s += "…" }
399
- return s
400
- }
401
-
402
- func enter() {
403
- inventory = buildInventory()
404
- chords = buildChords()
405
- desktopSnapshot = buildDesktopInventory()
406
- clearSelection()
407
- desktopMode = .browsing
408
- gridPreviewPlacement = nil
409
- phase = .desktopInventory
410
- configureLaunchMode()
411
- // Don't call onPanelResize here — caller handles initial sizing
412
- }
413
-
414
- /// Returns true if the key was consumed
415
- func handleKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
416
- // Backtick (keyCode 50) toggles desktop inventory from either phase
417
- if keyCode == 50 {
418
- if isSearching {
419
- deactivateSearch()
420
- return true
421
- }
422
- if phase == .desktopInventory {
423
- // Back to chord view
424
- clearSelection()
425
- desktopMode = .browsing
426
- activePreset = nil
427
- phase = .inventory
428
- onPanelResize?(chordPanelSize.0, chordPanelSize.1)
429
- return true
430
- } else if phase == .inventory {
431
- // Enter desktop inventory
432
- let diag = DiagnosticLog.shared
433
- desktopSnapshot = buildDesktopInventory()
434
- clearSelection()
435
- desktopMode = .browsing
436
- phase = .desktopInventory
437
- let size = desktopPanelSize
438
- onPanelResize?(size.0, size.1)
439
- if let snap = desktopSnapshot {
440
- let totalWindows = snap.allWindows.count
441
- let totalSpaces = snap.displays.reduce(0) { $0 + $1.spaces.count }
442
- diag.info("Desktop inventory: \(snap.displays.count) display(s), \(totalSpaces) space(s), \(totalWindows) window(s)")
443
- }
444
- return true
445
- }
446
- }
447
-
448
- // Route desktop inventory keys
449
- if phase == .desktopInventory {
450
- return handleDesktopInventoryKey(keyCode, modifiers: modifiers)
451
- }
452
-
453
- // Escape from chord view → dismiss
454
- if keyCode == 53 {
455
- dismiss()
456
- return true
457
- }
458
-
459
- guard phase == .inventory else { return false }
460
-
461
- // Check chord map
462
- if let chord = chords.first(where: { $0.keyCode == keyCode }) {
463
- phase = .executing(chord.label)
464
- let action = chord.action
465
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
466
- action()
467
- self?.dismiss()
468
- }
469
- return true
470
- }
471
-
472
- // Unknown key — ignore
473
- return true
474
- }
475
-
476
- // MARK: - Desktop Inventory Key Handling
477
-
478
- private func handleDesktopInventoryKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
479
- switch desktopMode {
480
- case .browsing: return handleBrowsingKey(keyCode, modifiers: modifiers)
481
- case .tiling: return handleTilingKey(keyCode, modifiers: modifiers)
482
- case .gridPreview: return handleGridPreviewKey(keyCode)
483
- case .screenMap: return true // handled by standalone ScreenMapWindowController
484
- }
485
- }
486
-
487
- // MARK: Browsing — ↑↓ within column, ←→ between displays, Enter → actions
488
-
489
- private func handleBrowsingKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
490
- // Cmd+A → select all visible windows (works during search too — selects filtered results)
491
- if keyCode == 0 && modifiers.contains(.command) {
492
- let allIds = Set(flatWindowList.map(\.id))
493
- if selectedWindowIds == allIds {
494
- clearSelection() // toggle off
495
- } else {
496
- selectedWindowIds = allIds
497
- }
498
- if isSearching { deactivateSearch() }
499
- return true
500
- }
501
-
502
- switch keyCode {
503
- case 53: // Escape — always dismiss
504
- onDismiss?()
505
- return true
506
-
507
- case 126: // ↑
508
- if modifiers.contains(.shift) {
509
- extendSelectionVertical(-1)
510
- } else {
511
- moveSelectionVertical(-1)
512
- }
513
- return true
514
-
515
- case 125: // ↓
516
- if modifiers.contains(.shift) {
517
- extendSelectionVertical(1)
518
- } else {
519
- moveSelectionVertical(1)
520
- }
521
- return true
522
-
523
- case 38: // j
524
- if isSearching { return false }
525
- if modifiers.contains(.shift) {
526
- extendSelectionVertical(1)
527
- } else {
528
- moveSelectionVertical(1)
529
- }
530
- return true
531
-
532
- case 40: // k
533
- if isSearching { return false }
534
- if modifiers.contains(.shift) {
535
- extendSelectionVertical(-1)
536
- } else {
537
- moveSelectionVertical(-1)
538
- }
539
- return true
540
-
541
- case 123: // ← → jump to previous display
542
- moveSelectionToDisplay(delta: -1)
543
- return true
544
-
545
- case 124: // → → jump to next display
546
- moveSelectionToDisplay(delta: 1)
547
- return true
548
-
549
- case 36: // Enter
550
- if isSearching {
551
- // Select first match and bring to front
552
- if let first = flatWindowList.first {
553
- selectSingle(first.id)
554
- bringSelectedToFront()
555
- }
556
- deactivateSearch()
557
- return true
558
- }
559
- if !selectedWindowIds.isEmpty {
560
- if selectedWindowIds.count > 1 {
561
- bringAllSelectedToFront()
562
- } else {
563
- bringSelectedToFront()
564
- }
565
- } else {
566
- moveSelectionVertical(1) // select first window
567
- }
568
- return true
569
-
570
- case 44: // / → activate search
571
- if !isSearching {
572
- activateSearch()
573
- return true
574
- }
575
- return false
576
-
577
- case 3: // f → focus window directly
578
- if isSearching && selectedWindowIds.isEmpty { return false }
579
- if isSearching { deactivateSearch() }
580
- if !selectedWindowIds.isEmpty {
581
- if selectedWindowIds.count > 1 {
582
- focusAllSelected()
583
- } else {
584
- focusSelectedWindow()
585
- }
586
- }
587
- return true
588
-
589
- case 17: // t → enter tiling mode directly
590
- if isSearching && selectedWindowIds.isEmpty { return false }
591
- if isSearching { deactivateSearch() }
592
- if !selectedWindowIds.isEmpty {
593
- desktopMode = .tiling
594
- }
595
- return true
596
-
597
- case 1: // s → grid preview (or show & distribute if single)
598
- if isSearching && selectedWindowIds.isEmpty { return false }
599
- if isSearching { deactivateSearch() }
600
- if !selectedWindowIds.isEmpty {
601
- gridPreviewPlacement = nil
602
- desktopMode = .gridPreview
603
- }
604
- return true
605
-
606
- case 4: // h → highlight window directly
607
- if isSearching && selectedWindowIds.isEmpty { return false }
608
- if isSearching { deactivateSearch() }
609
- if !selectedWindowIds.isEmpty {
610
- if selectedWindowIds.count > 1 {
611
- highlightAllSelected()
612
- } else {
613
- highlightSelectedWindow()
614
- }
615
- }
616
- return true
617
-
618
- case 2: // d → distribute selected
619
- if isSearching && selectedWindowIds.isEmpty { return false }
620
- if isSearching { deactivateSearch() }
621
- guard !selectedWindowIds.isEmpty else {
622
- flash("Select 2+ windows to organize")
623
- return true
624
- }
625
- guard selectedWindowIds.count > 1 else {
626
- flash("Add another window, then press D to organize")
627
- return true
628
- }
629
- distributeSelected()
630
- return true
631
-
632
- case 46: // m → screen map editor (standalone window)
633
- if isSearching { deactivateSearch() }
634
- ScreenMapWindowController.shared.show()
635
- return true
636
-
637
- case 18, 19, 20, 21, 23, 22: // 1-6 → filter presets (only when no selection and not searching)
638
- if isSearching { return false }
639
- if selectedWindowIds.isEmpty {
640
- let keyToIndex: [UInt16: Int] = [18: 1, 19: 2, 20: 3, 21: 4, 23: 5, 22: 6]
641
- if let idx = keyToIndex[keyCode], let preset = FilterPreset.from(keyIndex: idx) {
642
- if activePreset == preset {
643
- activePreset = nil // toggle off
644
- } else {
645
- activePreset = preset
646
- }
647
- clearSelection()
648
- }
649
- }
650
- return true
651
-
652
- default:
653
- if isSearching { return false }
654
- return true
655
- }
656
- }
657
-
658
- // MARK: Tiling — position keys
659
-
660
- private func handleTilingKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
661
- switch keyCode {
662
- case 53: // Escape — always dismiss
663
- onDismiss?()
664
- return true
665
-
666
- case 123: tileSelectedWindow(to: .left); return true // ←
667
- case 124: tileSelectedWindow(to: .right); return true // →
668
- case 126: // ↑ — shift=maximize, plain=top half
669
- if modifiers.contains(.shift) {
670
- tileSelectedWindow(to: .maximize)
671
- } else {
672
- tileSelectedWindow(to: .top)
673
- }
674
- return true
675
- case 125: tileSelectedWindow(to: .bottom); return true // ↓
676
- case 18: tileSelectedWindow(to: .topLeft); return true // 1
677
- case 19: tileSelectedWindow(to: .topRight); return true // 2
678
- case 20: tileSelectedWindow(to: .bottomLeft); return true // 3
679
- case 21: tileSelectedWindow(to: .bottomRight); return true// 4
680
- case 23: tileSelectedWindow(to: .leftThird); return true // 5
681
- case 22: tileSelectedWindow(to: .centerThird); return true// 6
682
- case 26: tileSelectedWindow(to: .rightThird); return true // 7
683
- case 8: tileSelectedWindow(to: .center); return true // c
684
- case 2: distributeSelectedHorizontally(); return true // d → distribute
685
-
686
- default:
687
- return true
688
- }
689
- }
690
-
691
- // MARK: Grid Preview — Enter/s to apply, Esc to cancel
692
-
693
- private func handleGridPreviewKey(_ keyCode: UInt16) -> Bool {
694
- switch keyCode {
695
- case 53: // Escape — cancel preview, keep selection
696
- gridPreviewPlacement = nil
697
- desktopMode = .browsing
698
- return true
699
-
700
- case 36, 1: // Enter or s → apply the layout
701
- showAndDistributeSelected(in: gridPreviewPlacement)
702
- gridPreviewPlacement = nil
703
- desktopMode = .browsing
704
- return true
705
-
706
- case 123:
707
- gridPreviewPlacement = .tile(.left)
708
- return true
709
- case 124:
710
- gridPreviewPlacement = .tile(.right)
711
- return true
712
- case 126:
713
- gridPreviewPlacement = .tile(.top)
714
- return true
715
- case 125:
716
- gridPreviewPlacement = .tile(.bottom)
717
- return true
718
- case 18:
719
- gridPreviewPlacement = .tile(.topLeft)
720
- return true
721
- case 19:
722
- gridPreviewPlacement = .tile(.topRight)
723
- return true
724
- case 20:
725
- gridPreviewPlacement = .tile(.bottomLeft)
726
- return true
727
- case 21:
728
- gridPreviewPlacement = .tile(.bottomRight)
729
- return true
730
- case 23:
731
- gridPreviewPlacement = .tile(.leftThird)
732
- return true
733
- case 22:
734
- gridPreviewPlacement = .tile(.centerThird)
735
- return true
736
- case 26:
737
- gridPreviewPlacement = .tile(.rightThird)
738
- return true
739
- case 8:
740
- gridPreviewPlacement = .tile(.center)
741
- return true
742
-
743
- default:
744
- return true
745
- }
746
- }
747
-
748
- /// Windows arranged in grid order for preview
749
- var gridPreviewWindows: [DesktopInventorySnapshot.InventoryWindowInfo] {
750
- flatWindowList.filter { selectedWindowIds.contains($0.id) }
751
- }
752
-
753
- /// Grid shape for current selection
754
- var gridPreviewShape: [Int] {
755
- WindowTiler.gridShape(for: selectedWindowIds.count)
756
- }
757
-
758
- // MARK: - Selection Actions
759
-
760
- /// Move selection up/down within the flat window list (stays in same display column when possible)
761
- private func moveSelectionVertical(_ delta: Int) {
762
- guard let snapshot = filteredSnapshot else { return }
763
-
764
- let anchor = cursorWindowId ?? selectedWindowId
765
- if let anchor = anchor,
766
- let displayIdx = displayIndex(for: anchor, in: snapshot) {
767
- let displayWindows = windowsInDisplay(displayIdx, snapshot: snapshot)
768
- if let localIdx = displayWindows.firstIndex(where: { $0.id == anchor }) {
769
- let newIdx = max(0, min(displayWindows.count - 1, localIdx + delta))
770
- selectSingle(displayWindows[newIdx].id)
771
- }
772
- } else {
773
- // No selection — pick first window in first display
774
- let windows = flatWindowList
775
- guard !windows.isEmpty else { return }
776
- if let id = delta > 0 ? windows.first?.id : windows.last?.id {
777
- selectSingle(id)
778
- }
779
- }
780
-
781
- if let wid = cursorWindowId, let win = flatWindowList.first(where: { $0.id == wid }) {
782
- let title = win.title.isEmpty ? "(untitled)" : String(win.title.prefix(30))
783
- DiagnosticLog.shared.info("Select: wid=\(wid) \"\(title)\"")
784
- }
785
- }
786
-
787
- /// Extend selection up/down (Shift+arrow) — adds items without removing existing selection
788
- private func extendSelectionVertical(_ delta: Int) {
789
- guard let snapshot = filteredSnapshot else { return }
790
-
791
- let anchor = cursorWindowId ?? selectedWindowId
792
- if let anchor = anchor,
793
- let displayIdx = displayIndex(for: anchor, in: snapshot) {
794
- let displayWindows = windowsInDisplay(displayIdx, snapshot: snapshot)
795
- if let localIdx = displayWindows.firstIndex(where: { $0.id == anchor }) {
796
- let newIdx = max(0, min(displayWindows.count - 1, localIdx + delta))
797
- let newId = displayWindows[newIdx].id
798
- selectedWindowIds.insert(newId)
799
- cursorWindowId = newId
800
- }
801
- } else {
802
- let windows = flatWindowList
803
- guard !windows.isEmpty else { return }
804
- if let id = delta > 0 ? windows.first?.id : windows.last?.id {
805
- selectedWindowIds.insert(id)
806
- cursorWindowId = id
807
- }
808
- }
809
- }
810
-
811
- /// Jump selection to the adjacent display column
812
- private func moveSelectionToDisplay(delta: Int) {
813
- guard let snapshot = filteredSnapshot, snapshot.displays.count > 1 else { return }
814
-
815
- let displayCount = snapshot.displays.count
816
-
817
- // Find current display index
818
- let currentDisplayIdx: Int
819
- if let wid = selectedWindowId, let idx = displayIndex(for: wid, in: snapshot) {
820
- currentDisplayIdx = idx
821
- } else {
822
- // No selection — start from first or last display
823
- currentDisplayIdx = delta > 0 ? -1 : displayCount
824
- }
825
-
826
- let targetIdx = currentDisplayIdx + delta
827
- guard targetIdx >= 0, targetIdx < displayCount else { return }
828
-
829
- // Find the position in the current display for context
830
- let targetWindows = windowsInDisplay(targetIdx, snapshot: snapshot)
831
- guard !targetWindows.isEmpty else { return }
832
-
833
- // Try to land at a similar position (same row index)
834
- if let wid = selectedWindowId,
835
- let srcIdx = displayIndex(for: wid, in: snapshot) {
836
- let srcWindows = windowsInDisplay(srcIdx, snapshot: snapshot)
837
- let srcPos = srcWindows.firstIndex(where: { $0.id == wid }) ?? 0
838
- let targetPos = min(srcPos, targetWindows.count - 1)
839
- selectSingle(targetWindows[targetPos].id)
840
- } else if let id = targetWindows.first?.id {
841
- selectSingle(id)
842
- }
843
-
844
- DiagnosticLog.shared.info("Jump to display \(targetIdx + 1)")
845
- }
846
-
847
- // MARK: - Display Helpers
848
-
849
- /// Get the display index for a given window ID
850
- private func displayIndex(for wid: UInt32, in snapshot: DesktopInventorySnapshot) -> Int? {
851
- for (dIdx, display) in snapshot.displays.enumerated() {
852
- for space in display.spaces {
853
- for app in space.apps {
854
- if app.windows.contains(where: { $0.id == wid }) {
855
- return dIdx
856
- }
857
- }
858
- }
859
- }
860
- return nil
861
- }
862
-
863
- /// Get all windows in a display as a flat list (preserving space/app order)
864
- private func windowsInDisplay(_ displayIdx: Int, snapshot: DesktopInventorySnapshot) -> [DesktopInventorySnapshot.InventoryWindowInfo] {
865
- guard displayIdx < snapshot.displays.count else { return [] }
866
- return snapshot.displays[displayIdx].spaces.flatMap { $0.apps.flatMap { $0.windows } }
867
- }
868
-
869
- private func bringSelectedToFront() {
870
- guard let wid = selectedWindowId,
871
- let window = flatWindowList.first(where: { $0.id == wid }) else { return }
872
- DiagnosticLog.shared.info("Front: wid=\(wid) pid=\(window.pid)")
873
- WindowTiler.raiseWindowAndReactivate(wid: wid, pid: window.pid)
874
- }
875
-
876
- private func bringAllSelectedToFront() {
877
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
878
- guard !windows.isEmpty else { return }
879
- DiagnosticLog.shared.info("Front all: \(windows.count) windows")
880
- WindowTiler.raiseWindowsAndReactivate(windows: windows.map { (wid: $0.id, pid: $0.pid) })
881
- }
882
-
883
- private func focusSelectedWindow() {
884
- guard let wid = selectedWindowId,
885
- let window = flatWindowList.first(where: { $0.id == wid }) else { return }
886
- DiagnosticLog.shared.info("Focus: wid=\(wid) pid=\(window.pid)")
887
- WindowTiler.raiseWindowAndReactivate(wid: wid, pid: window.pid)
888
- }
889
-
890
- private func highlightSelectedWindow() {
891
- guard let wid = selectedWindowId else { return }
892
- DiagnosticLog.shared.info("Highlight: wid=\(wid)")
893
- WindowTiler.highlightWindowById(wid: wid)
894
- }
895
-
896
- private func tileSelectedWindow(to position: TilePosition) {
897
- if selectedWindowIds.count > 1 {
898
- tileAllSelected(to: position)
899
- return
900
- }
901
- guard let wid = selectedWindowId,
902
- let window = flatWindowList.first(where: { $0.id == wid }) else { return }
903
-
904
- DiagnosticLog.shared.info("Tile: wid=\(wid) → \(position.rawValue)")
905
- WindowTiler.tileWindowById(wid: wid, pid: window.pid, to: position)
906
-
907
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
908
- self?.desktopSnapshot = self?.buildDesktopInventory()
909
- }
910
- }
911
-
912
- private func tileAllSelected(to position: TilePosition) {
913
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
914
- guard !windows.isEmpty else { return }
915
- DiagnosticLog.shared.info("Grid selected \(windows.count): \(position.rawValue)")
916
- showAndDistributeSelected(in: .tile(position))
917
- }
918
-
919
- private func distributeSelectedHorizontally() {
920
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
921
- guard windows.count >= 2 else { return }
922
- DiagnosticLog.shared.info("Distribute H: \(windows.count) windows")
923
- WindowTiler.tileDistributeHorizontally(windows: windows.map { (wid: $0.id, pid: $0.pid) })
924
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
925
- self?.desktopSnapshot = self?.buildDesktopInventory()
926
- }
927
- }
928
-
929
- // MARK: - Batch Actions (multi-select)
930
-
931
- func focusAllSelected() {
932
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
933
- guard !windows.isEmpty else { return }
934
- DiagnosticLog.shared.info("Focus all: \(windows.count) windows")
935
- WindowTiler.raiseWindowsAndReactivate(windows: windows.map { (wid: $0.id, pid: $0.pid) })
936
- }
937
-
938
- func highlightAllSelected() {
939
- let wids = flatWindowList.filter { selectedWindowIds.contains($0.id) }.map(\.id)
940
- guard !wids.isEmpty else { return }
941
- DiagnosticLog.shared.info("Highlight all: \(wids.count) windows")
942
- for wid in wids {
943
- WindowTiler.highlightWindowById(wid: wid)
944
- }
945
- }
946
-
947
- /// Show all selected windows (raise to front) without changing layout
948
- func showAllSelected() {
949
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
950
- guard !windows.isEmpty else { return }
951
- savePositions(for: windows)
952
- WindowTiler.raiseWindowsAndReactivate(windows: windows.map { (wid: $0.id, pid: $0.pid) })
953
- flash("Showing \(windows.count) window\(windows.count == 1 ? "" : "s")")
954
- }
955
-
956
- /// Show all selected windows AND distribute in smart grid — single batch operation
957
- func showAndDistributeSelected(in placement: PlacementSpec? = nil) {
958
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
959
- guard !windows.isEmpty else { return }
960
- savePositions(for: windows)
961
- WindowTiler.batchRaiseAndDistribute(
962
- windows: windows.map { (wid: $0.id, pid: $0.pid) },
963
- region: placement?.fractions
964
- )
965
- let shape = WindowTiler.gridShape(for: windows.count)
966
- let grid = shape.map(String.init).joined(separator: "+")
967
- let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
968
- flash("\(windows.count) windows\(region) [\(grid)]")
969
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
970
- self?.desktopSnapshot = self?.buildDesktopInventory()
971
- }
972
- }
973
-
974
- /// Distribute selected in smart grid without raising
975
- func distributeSelected(in placement: PlacementSpec? = nil) {
976
- let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
977
- guard !windows.isEmpty else { return }
978
- savePositions(for: windows)
979
- WindowTiler.batchRaiseAndDistribute(
980
- windows: windows.map { (wid: $0.id, pid: $0.pid) },
981
- region: placement?.fractions
982
- )
983
- let shape = WindowTiler.gridShape(for: windows.count)
984
- let grid = shape.map(String.init).joined(separator: "+")
985
- let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
986
- flash("\(windows.count) windows\(region) [\(grid)]")
987
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
988
- self?.desktopSnapshot = self?.buildDesktopInventory()
989
- }
990
- }
991
-
992
- /// Save current positions of windows so they can be restored later
993
- private func savePositions(for windows: [DesktopInventorySnapshot.InventoryWindowInfo]) {
994
- // Don't overwrite if already saved (allow chaining actions)
995
- guard savedPositions == nil else { return }
996
- var positions: [UInt32: (pid: Int32, frame: WindowFrame)] = [:]
997
- for win in windows {
998
- positions[win.id] = (pid: win.pid, frame: win.frame)
999
- }
1000
- savedPositions = positions
1001
- DiagnosticLog.shared.info("Saved positions for \(positions.count) windows")
1002
- }
1003
-
1004
- /// Restore windows to their saved positions — single batch operation
1005
- func restorePositions() {
1006
- guard let positions = savedPositions else { return }
1007
- DiagnosticLog.shared.info("Restoring \(positions.count) window positions")
1008
- let restores = positions.map { (wid: $0.key, pid: $0.value.pid, frame: $0.value.frame) }
1009
- WindowTiler.batchRestoreWindows(restores)
1010
- savedPositions = nil
1011
- flash("Restored \(restores.count) window\(restores.count == 1 ? "" : "s")")
1012
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
1013
- self?.desktopSnapshot = self?.buildDesktopInventory()
1014
- }
1015
- }
1016
-
1017
- /// Accept the current layout — discard saved positions
1018
- func discardSavedPositions() {
1019
- savedPositions = nil
1020
- DiagnosticLog.shared.info("Accepted layout, discarded saved positions")
1021
- }
1022
-
1023
- /// Show a brief flash message that auto-dismisses
1024
- func flash(_ message: String) {
1025
- flashMessage = message
1026
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
1027
- if self?.flashMessage == message { self?.flashMessage = nil }
1028
- }
1029
- }
1030
-
1031
- /// Copy a text representation of the desktop inventory to clipboard
1032
- func copyInventoryToClipboard() {
1033
- guard let snapshot = desktopSnapshot else { return }
1034
- var lines: [String] = ["DESKTOP INVENTORY"]
1035
- lines.append(String(repeating: "─", count: 60))
1036
-
1037
- for display in snapshot.displays {
1038
- lines.append("")
1039
- lines.append("\(display.name) \(display.visibleFrame.w)×\(display.visibleFrame.h) (\(display.spaceCount) spaces)")
1040
- for space in display.spaces {
1041
- let tag = space.isCurrent ? " ◀ active" : ""
1042
- let winCount = space.apps.reduce(0) { $0 + $1.windows.count }
1043
- lines.append(" Space \(space.index)\(tag) (\(winCount) windows)")
1044
- for app in space.apps {
1045
- if app.windows.count == 1, let win = app.windows.first {
1046
- let tile = win.tilePosition?.label ?? "—"
1047
- let title = win.title.isEmpty ? "(untitled)" : win.title
1048
- let dmx = win.isLattices ? " [lattices]" : ""
1049
- let path = win.inventoryPath?.description ?? ""
1050
- lines.append(" \(app.appName) \(title)\(dmx) \(Int(win.frame.w))×\(Int(win.frame.h)) \(tile) \(path)")
1051
- } else {
1052
- lines.append(" \(app.appName)")
1053
- for win in app.windows {
1054
- let tile = win.tilePosition?.label ?? "—"
1055
- let title = win.title.isEmpty ? "(untitled)" : win.title
1056
- let dmx = win.isLattices ? " [lattices]" : ""
1057
- let path = win.inventoryPath?.description ?? ""
1058
- lines.append(" \(title)\(dmx) \(Int(win.frame.w))×\(Int(win.frame.h)) \(tile) \(path)")
1059
- }
1060
- }
1061
- }
1062
- }
1063
- }
1064
-
1065
- let text = lines.joined(separator: "\n")
1066
- NSPasteboard.general.clearContents()
1067
- NSPasteboard.general.setString(text, forType: .string)
1068
- DiagnosticLog.shared.success("Copied inventory to clipboard (\(text.count) chars)")
1069
- }
1070
-
1071
- func dismiss() {
1072
- phase = .idle
1073
- onDismiss?()
1074
- }
1075
-
1076
- private func syncSharedSelection() {
1077
- guard !selectedWindowIds.isEmpty else {
1078
- WindowSelectionStore.shared.clear(source: "desktop-inventory")
1079
- return
1080
- }
1081
-
1082
- let summaries = flatWindowList
1083
- .filter { selectedWindowIds.contains($0.id) }
1084
- .map {
1085
- SelectedWindowSummary(
1086
- wid: $0.id,
1087
- app: $0.appName ?? "Window",
1088
- title: $0.title,
1089
- latticesSession: $0.latticesSession
1090
- )
1091
- }
1092
-
1093
- guard !summaries.isEmpty else { return }
1094
- WindowSelectionStore.shared.setSelection(summaries, source: "desktop-inventory")
1095
- }
1096
-
1097
- private func regionLabel(for placement: PlacementSpec) -> String {
1098
- switch placement {
1099
- case .tile(let position):
1100
- return position.label
1101
- default:
1102
- return placement.wireValue
1103
- }
1104
- }
1105
-
1106
- private func configureLaunchMode() {
1107
- switch launchMode {
1108
- case .normal:
1109
- return
1110
- case .organize(let appName):
1111
- activePreset = .currentSpace
1112
- seedSelectionForOrganization(appName: appName)
1113
- }
1114
- }
1115
-
1116
- private func seedSelectionForOrganization(appName: String?) {
1117
- let visibleWindows = flatWindowList.filter(\.isOnScreen)
1118
- let targetApp = appName ?? visibleWindows.first?.appName
1119
- let initialSelection = visibleWindows.filter { window in
1120
- guard let name = window.appName, let targetApp else { return false }
1121
- return name.localizedCaseInsensitiveCompare(targetApp) == .orderedSame
1122
- }
1123
-
1124
- if initialSelection.isEmpty {
1125
- flash("Select windows to organize. Cmd-click adds or removes windows; D distributes.")
1126
- return
1127
- }
1128
-
1129
- selectedWindowIds = Set(initialSelection.map(\.id))
1130
- cursorWindowId = initialSelection.first?.id
1131
-
1132
- if let targetApp {
1133
- if initialSelection.count > 1 {
1134
- flash("Selected \(initialSelection.count) \(targetApp) windows. Press D to organize.")
1135
- } else {
1136
- flash("Selected the \(targetApp) window. Cmd-click more windows, then press D.")
1137
- }
1138
- } else if initialSelection.count > 1 {
1139
- flash("Selected \(initialSelection.count) windows. Press D to organize.")
1140
- } else {
1141
- flash("Selected 1 window. Cmd-click more windows, then press D.")
1142
- }
1143
-
1144
- DispatchQueue.main.async { [weak self] in
1145
- guard let self = self else { return }
1146
- if self.selectedWindowIds.count > 1 {
1147
- self.highlightAllSelected()
1148
- } else {
1149
- self.highlightSelectedWindow()
1150
- }
1151
- }
1152
- }
1153
-
1154
- // MARK: - Inventory Builder
1155
-
1156
- private func buildInventory() -> CommandModeInventory {
1157
- let workspace = WorkspaceManager.shared
1158
- let tmux = TmuxModel.shared
1159
- let inventoryMgr = InventoryManager.shared
1160
-
1161
- // Refresh inventory so orphans are current
1162
- inventoryMgr.refresh()
1163
-
1164
- let activeLayer = workspace.activeLayer
1165
- let layerCount = workspace.config?.layers?.count ?? 0
1166
-
1167
- var items: [CommandModeInventory.Item] = []
1168
-
1169
- // Active layer projects
1170
- if let layer = activeLayer {
1171
- for lp in layer.projects {
1172
- if let groupId = lp.group, let group = workspace.group(byId: groupId) {
1173
- let running = workspace.isGroupRunning(group)
1174
- let paneCount = group.tabs.count
1175
- items.append(.init(
1176
- name: group.label,
1177
- group: "Layer: \(layer.label)",
1178
- status: running ? .running : .stopped,
1179
- paneCount: paneCount,
1180
- tileHint: lp.tile
1181
- ))
1182
- } else if let path = lp.path {
1183
- let name = (path as NSString).lastPathComponent
1184
- let sessionName = WorkspaceManager.sessionName(for: path)
1185
- let session = tmux.sessions.first(where: { $0.name == sessionName })
1186
- let status: CommandModeInventory.Status
1187
- if let s = session {
1188
- status = s.attached ? .attached : .running
1189
- } else {
1190
- status = .stopped
1191
- }
1192
- items.append(.init(
1193
- name: name,
1194
- group: "Layer: \(layer.label)",
1195
- status: status,
1196
- paneCount: session?.panes.count ?? 0,
1197
- tileHint: lp.tile
1198
- ))
1199
- }
1200
- }
1201
- }
1202
-
1203
- // Tab groups not in active layer
1204
- if let groups = workspace.config?.groups {
1205
- let layerGroupIds = Set(activeLayer?.projects.compactMap(\.group) ?? [])
1206
- for group in groups where !layerGroupIds.contains(group.id) {
1207
- let running = workspace.isGroupRunning(group)
1208
- items.append(.init(
1209
- name: group.label,
1210
- group: "Group: \(group.label)",
1211
- status: running ? .running : .stopped,
1212
- paneCount: group.tabs.count,
1213
- tileHint: nil
1214
- ))
1215
- }
1216
- }
1217
-
1218
- // Orphans
1219
- for orphan in inventoryMgr.orphans {
1220
- items.append(.init(
1221
- name: orphan.name,
1222
- group: "Orphan",
1223
- status: orphan.attached ? .attached : .running,
1224
- paneCount: orphan.panes.count,
1225
- tileHint: nil
1226
- ))
1227
- }
1228
-
1229
- return CommandModeInventory(
1230
- activeLayer: activeLayer?.label,
1231
- layerCount: layerCount,
1232
- items: items
1233
- )
1234
- }
1235
-
1236
- // MARK: - Desktop Inventory Builder
1237
-
1238
- private func buildDesktopInventory() -> DesktopInventorySnapshot {
1239
- let originalScreens = NSScreen.screens
1240
- let displaySpaces = WindowTiler.getDisplaySpaces()
1241
- let primaryHeight = originalScreens.first?.frame.height ?? 0
1242
-
1243
- // Sort screens left-to-right by frame origin, tie-break top-to-bottom
1244
- let sortedScreens = originalScreens.sorted {
1245
- if $0.frame.origin.x != $1.frame.origin.x {
1246
- return $0.frame.origin.x < $1.frame.origin.x
1247
- }
1248
- return $0.frame.origin.y > $1.frame.origin.y
1249
- }
1250
- // Map sorted index → original index for displaySpaces lookup
1251
- let sortedToOriginal = sortedScreens.map { s in originalScreens.firstIndex(where: { $0 === s })! }
1252
- let screens = sortedScreens
1253
-
1254
- // Build space-to-display mapping: spaceId → (displayIndex, spaceIndex)
1255
- var spaceToDisplay: [Int: (displayIdx: Int, spaceIdx: Int)] = [:]
1256
- for (dIdx, ds) in displaySpaces.enumerated() {
1257
- for space in ds.spaces {
1258
- spaceToDisplay[space.id] = (dIdx, space.index)
1259
- }
1260
- }
1261
-
1262
- // Current space IDs per display
1263
- let currentSpaceIds = Set(displaySpaces.map(\.currentSpaceId))
1264
-
1265
- // Query ALL windows (not just on-screen) to capture every space
1266
- guard let rawList = CGWindowListCopyWindowInfo(
1267
- [.optionAll, .excludeDesktopElements],
1268
- kCGNullWindowID
1269
- ) as? [[String: Any]] else {
1270
- return DesktopInventorySnapshot(displays: [], timestamp: Date())
1271
- }
1272
-
1273
- // Parse raw CG window info
1274
- struct RawWindow {
1275
- let wid: UInt32; let app: String; let pid: Int32
1276
- let title: String; let frame: WindowFrame
1277
- let latticesSession: String?; let spaceIds: [Int]
1278
- }
1279
-
1280
- // System/helper processes that create layer-0 windows users don't care about
1281
- let blockedApps: Set<String> = [
1282
- // macOS system
1283
- "WindowServer", "Dock", "SystemUIServer", "Control Center",
1284
- "Notification Center", "NotificationCenter", "Spotlight", "WindowManager",
1285
- "TextInputMenuAgent", "TextInputSwitcher", "universalAccessAuthWarn",
1286
- "AXVisualSupportAgent", "loginwindow", "ScreenSaverEngine",
1287
- // UI service helpers (run as XPC, show popover/autofill UI)
1288
- "AutoFill", "AuthenticationServicesHelper", "CursorUIViewService",
1289
- "SharedWebCredentialViewService", "CoreServicesUIAgent",
1290
- "UserNotificationCenter", "SecurityAgent", "OSDUIHelper",
1291
- "PassKit UIService", "QuickLookUIService", "ScopedBookmarkAgent",
1292
- // Dev tool helpers
1293
- "Instruments", "FileMerge",
1294
- ]
1295
- // Also block apps whose name ends with known helper suffixes
1296
- let blockedSuffixes = ["UIService", "UIHelper", "Agent", "Helper", "ViewService"]
1297
-
1298
- let ownPid = ProcessInfo.processInfo.processIdentifier
1299
- let rawCount = rawList.count
1300
-
1301
- var allWindows: [RawWindow] = []
1302
- for info in rawList {
1303
- guard let wid = info[kCGWindowNumber as String] as? UInt32,
1304
- let ownerName = info[kCGWindowOwnerName as String] as? String,
1305
- let pid = info[kCGWindowOwnerPID as String] as? Int32,
1306
- let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
1307
- else { continue }
1308
-
1309
- // Skip our own windows
1310
- guard pid != ownPid else { continue }
1311
-
1312
- // Skip known system/helper processes
1313
- guard !blockedApps.contains(ownerName) else { continue }
1314
- if blockedSuffixes.contains(where: { ownerName.hasSuffix($0) }) { continue }
1315
-
1316
- var rect = CGRect.zero
1317
- guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect),
1318
- rect.width >= 100, rect.height >= 50 else { continue }
1319
-
1320
- let layer = info[kCGWindowLayer as String] as? Int ?? 0
1321
- guard layer == 0 else { continue }
1322
-
1323
- let title = info[kCGWindowName as String] as? String ?? ""
1324
- let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? false
1325
- let spaceIds = WindowTiler.getSpacesForWindow(wid)
1326
-
1327
- // Skip windows not assigned to any space (background helpers)
1328
- guard !spaceIds.isEmpty else { continue }
1329
-
1330
- // For windows on a current space, require them to be actually visible.
1331
- // This filters hidden helper windows (AutoFill, CursorUIViewService, etc.)
1332
- // while keeping real windows on other spaces.
1333
- let isOnCurrentSpace = spaceIds.contains(where: { currentSpaceIds.contains($0) })
1334
- if isOnCurrentSpace && !isOnScreen { continue }
1335
-
1336
- let frame = WindowFrame(x: Double(rect.origin.x), y: Double(rect.origin.y),
1337
- w: Double(rect.width), h: Double(rect.height))
1338
-
1339
- var latticesSession: String?
1340
- if let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) {
1341
- let match = String(title[range])
1342
- latticesSession = String(match.dropFirst(9).dropLast(1))
1343
- }
1344
-
1345
- allWindows.append(RawWindow(wid: wid, app: ownerName, pid: pid, title: title,
1346
- frame: frame, latticesSession: latticesSession, spaceIds: spaceIds))
1347
- }
1348
-
1349
- DiagnosticLog.shared.info("Desktop scan: \(rawCount) raw → \(allWindows.count) after filter")
1350
-
1351
- // Assign each window to (display, space)
1352
- struct AssignedWindow {
1353
- let win: RawWindow; let displayIdx: Int; let spaceId: Int; let spaceIdx: Int; let isOnScreen: Bool
1354
- }
1355
-
1356
- var assigned: [AssignedWindow] = []
1357
- for win in allWindows {
1358
- // Primary: use space→display mapping
1359
- for sid in win.spaceIds {
1360
- if let mapping = spaceToDisplay[sid] {
1361
- assigned.append(AssignedWindow(
1362
- win: win,
1363
- displayIdx: mapping.displayIdx,
1364
- spaceId: sid,
1365
- spaceIdx: mapping.spaceIdx,
1366
- isOnScreen: currentSpaceIds.contains(sid)
1367
- ))
1368
- break // assign to first known space
1369
- }
1370
- }
1371
-
1372
- // Fallback: match by frame center (no space info)
1373
- if !win.spaceIds.contains(where: { spaceToDisplay[$0] != nil }) {
1374
- let cx = win.frame.x + win.frame.w / 2
1375
- let cy = win.frame.y + win.frame.h / 2
1376
- let nsCy = primaryHeight - cy
1377
- for (sIdx, screen) in screens.enumerated() {
1378
- if screen.frame.contains(NSPoint(x: cx, y: nsCy)) {
1379
- let origIdx = sortedToOriginal[sIdx]
1380
- let ds = origIdx < displaySpaces.count ? displaySpaces[origIdx] : nil
1381
- let currentSid = ds?.currentSpaceId ?? 0
1382
- let currentIdx = ds?.spaces.first(where: { $0.isCurrent })?.index ?? 1
1383
- assigned.append(AssignedWindow(
1384
- win: win, displayIdx: origIdx,
1385
- spaceId: currentSid, spaceIdx: currentIdx, isOnScreen: true
1386
- ))
1387
- break
1388
- }
1389
- }
1390
- }
1391
- }
1392
-
1393
- // Build hierarchical: Display → Space → App → Windows
1394
- var displays: [DesktopInventorySnapshot.DisplayInfo] = []
1395
-
1396
- for (screenIdx, screen) in screens.enumerated() {
1397
- let frame = screen.frame
1398
- let visible = screen.visibleFrame
1399
- let name = screen.localizedName
1400
-
1401
- let originalIdx = sortedToOriginal[screenIdx]
1402
- let ds = originalIdx < displaySpaces.count ? displaySpaces[originalIdx] : nil
1403
- let spaceCount = ds?.spaces.count ?? 1
1404
- let currentSpaceIdx = ds?.spaces.first(where: { $0.isCurrent })?.index ?? 1
1405
-
1406
- let screenWindows = assigned.filter { $0.displayIdx == originalIdx }
1407
-
1408
- // Group by space
1409
- var windowsBySpace: [Int: [AssignedWindow]] = [:]
1410
- for aw in screenWindows {
1411
- windowsBySpace[aw.spaceId, default: []].append(aw)
1412
- }
1413
-
1414
- // Build SpaceGroups sorted by space index
1415
- let isMain = screen == NSScreen.main
1416
- let displayLabel = InventoryPath.displayName(for: screen, isMain: isMain)
1417
- var spaceGroups: [DesktopInventorySnapshot.SpaceGroup] = []
1418
- let allSpacesForDisplay = ds?.spaces ?? []
1419
-
1420
- for spaceInfo in allSpacesForDisplay {
1421
- let spaceWindows = windowsBySpace[spaceInfo.id] ?? []
1422
- guard !spaceWindows.isEmpty else { continue }
1423
-
1424
- // Group by app within space
1425
- var appGroups: [String: [AssignedWindow]] = [:]
1426
- for aw in spaceWindows {
1427
- appGroups[aw.win.app, default: []].append(aw)
1428
- }
1429
-
1430
- var groups: [DesktopInventorySnapshot.AppGroup] = []
1431
- for appName in appGroups.keys.sorted() {
1432
- let wins = appGroups[appName]!
1433
- let appType = AppTypeClassifier.classify(appName)
1434
- let inventoryWindows = wins.map { aw -> DesktopInventorySnapshot.InventoryWindowInfo in
1435
- let tile = aw.isOnScreen ? WindowTiler.inferTilePosition(frame: aw.win.frame, screen: screen) : nil
1436
- let path = InventoryPath(
1437
- display: displayLabel,
1438
- space: "space\(aw.spaceIdx)",
1439
- appType: appType.rawValue,
1440
- appName: appName,
1441
- windowTitle: aw.win.title.isEmpty ? "untitled" : aw.win.title
1442
- )
1443
- return DesktopInventorySnapshot.InventoryWindowInfo(
1444
- id: aw.win.wid,
1445
- pid: aw.win.pid,
1446
- title: aw.win.title,
1447
- frame: aw.win.frame,
1448
- tilePosition: tile,
1449
- isLattices: aw.win.latticesSession != nil,
1450
- latticesSession: aw.win.latticesSession,
1451
- spaceIndex: aw.spaceIdx,
1452
- isOnScreen: aw.isOnScreen,
1453
- inventoryPath: path,
1454
- appName: appName
1455
- )
1456
- }
1457
- groups.append(DesktopInventorySnapshot.AppGroup(
1458
- id: "\(spaceInfo.id)-\(appName)",
1459
- appName: appName,
1460
- windows: inventoryWindows
1461
- ))
1462
- }
1463
-
1464
- spaceGroups.append(DesktopInventorySnapshot.SpaceGroup(
1465
- id: spaceInfo.id,
1466
- index: spaceInfo.index,
1467
- isCurrent: spaceInfo.isCurrent,
1468
- apps: groups
1469
- ))
1470
- }
1471
-
1472
- displays.append(DesktopInventorySnapshot.DisplayInfo(
1473
- id: ds?.displayId ?? "display-\(screenIdx)",
1474
- name: name,
1475
- resolution: (w: Int(frame.width), h: Int(frame.height)),
1476
- visibleFrame: (w: Int(visible.width), h: Int(visible.height)),
1477
- isMain: isMain,
1478
- spaceCount: spaceCount,
1479
- currentSpaceIndex: currentSpaceIdx,
1480
- spaces: spaceGroups
1481
- ))
1482
- }
1483
-
1484
- return DesktopInventorySnapshot(displays: displays, timestamp: Date())
1485
- }
1486
-
1487
- // MARK: - Chord Map
1488
-
1489
- private func buildChords() -> [Chord] {
1490
- let workspace = WorkspaceManager.shared
1491
-
1492
- var chords: [Chord] = []
1493
-
1494
- // [a] tile all — re-tile active layer's windows
1495
- chords.append(Chord(key: "a", keyCode: 0, label: "tile all") {
1496
- WorkspaceManager.shared.retileCurrentLayer()
1497
- })
1498
-
1499
- // [s] split — tile two most recent left/right
1500
- chords.append(Chord(key: "s", keyCode: 1, label: "split") {
1501
- let running = ProjectScanner.shared.projects.filter(\.isRunning)
1502
- let term = Preferences.shared.terminal
1503
- if running.count >= 2 {
1504
- WindowTiler.tile(session: running[0].sessionName, terminal: term, to: .left)
1505
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
1506
- WindowTiler.tile(session: running[1].sessionName, terminal: term, to: .right)
1507
- }
1508
- } else if running.count == 1 {
1509
- WindowTiler.tile(session: running[0].sessionName, terminal: term, to: .maximize)
1510
- }
1511
- })
1512
-
1513
- // [m] maximize — maximize frontmost terminal
1514
- chords.append(Chord(key: "m", keyCode: 46, label: "maximize") {
1515
- let term = Preferences.shared.terminal
1516
- // Find frontmost running project
1517
- let running = ProjectScanner.shared.projects.filter(\.isRunning)
1518
- if let first = running.first {
1519
- WindowTiler.tile(session: first.sessionName, terminal: term, to: .maximize)
1520
- }
1521
- })
1522
-
1523
- // [1]-[3] layer focus (dynamic)
1524
- let layers = workspace.config?.layers ?? []
1525
- let layerKeyCodes: [UInt16] = [18, 19, 20] // 1, 2, 3
1526
- for (i, layer) in layers.prefix(3).enumerated() {
1527
- let idx = i
1528
- chords.append(Chord(key: "\(i + 1)", keyCode: layerKeyCodes[i], label: layer.label.lowercased()) {
1529
- WorkspaceManager.shared.tileLayer(index: idx)
1530
- })
1531
- }
1532
-
1533
- // [l] launch layer — explicitly start non-running projects
1534
- chords.append(Chord(key: "l", keyCode: 37, label: "launch layer") {
1535
- let ws = WorkspaceManager.shared
1536
- ws.tileLayer(index: ws.activeLayerIndex, launch: true, force: true)
1537
- })
1538
-
1539
- // [r] refresh
1540
- chords.append(Chord(key: "r", keyCode: 15, label: "refresh") {
1541
- ProjectScanner.shared.scan()
1542
- TmuxModel.shared.poll()
1543
- InventoryManager.shared.refresh()
1544
- })
1545
-
1546
- // [p] palette
1547
- chords.append(Chord(key: "p", keyCode: 35, label: "palette") {
1548
- CommandPaletteWindow.shared.show()
1549
- })
1550
-
1551
- return chords
1552
- }
1553
- }
1554
-
1555
- private extension Sequence where Element == String {
1556
- func uniquePrefix(_ count: Int) -> [String] {
1557
- var seen = Set<String>()
1558
- var result: [String] = []
1559
- for item in self where !seen.contains(item) {
1560
- seen.insert(item)
1561
- result.append(item)
1562
- if result.count == count { break }
1563
- }
1564
- return result
1565
- }
1566
- }