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