@arach/lattices 0.1.0 → 0.2.1

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 (39) hide show
  1. package/README.md +101 -90
  2. package/app/Sources/ActionRow.swift +61 -0
  3. package/app/Sources/App.swift +1 -40
  4. package/app/Sources/AppDelegate.swift +154 -24
  5. package/app/Sources/CheatSheetHUD.swift +1 -0
  6. package/app/Sources/CommandModeState.swift +40 -19
  7. package/app/Sources/CommandModeView.swift +27 -2
  8. package/app/Sources/DaemonServer.swift +8 -0
  9. package/app/Sources/DiagnosticLog.swift +19 -1
  10. package/app/Sources/EventBus.swift +1 -0
  11. package/app/Sources/HotkeyManager.swift +1 -0
  12. package/app/Sources/HotkeyStore.swift +9 -1
  13. package/app/Sources/LatticesApi.swift +210 -0
  14. package/app/Sources/MainView.swift +46 -86
  15. package/app/Sources/MainWindow.swift +13 -0
  16. package/app/Sources/OcrModel.swift +309 -0
  17. package/app/Sources/OcrStore.swift +295 -0
  18. package/app/Sources/OmniSearchState.swift +283 -0
  19. package/app/Sources/OmniSearchView.swift +288 -0
  20. package/app/Sources/OmniSearchWindow.swift +105 -0
  21. package/app/Sources/PaletteCommand.swift +11 -1
  22. package/app/Sources/PermissionChecker.swift +12 -2
  23. package/app/Sources/Preferences.swift +44 -0
  24. package/app/Sources/ScreenMapState.swift +7 -17
  25. package/app/Sources/ScreenMapView.swift +3 -0
  26. package/app/Sources/SettingsView.swift +534 -122
  27. package/app/Sources/Theme.swift +39 -0
  28. package/app/Sources/WindowTiler.swift +59 -56
  29. package/bin/lattices-app.js +23 -7
  30. package/bin/lattices.js +123 -0
  31. package/docs/api.md +390 -249
  32. package/docs/app.md +75 -28
  33. package/docs/concepts.md +45 -136
  34. package/docs/config.md +8 -7
  35. package/docs/layers.md +16 -18
  36. package/docs/ocr.md +185 -0
  37. package/docs/overview.md +39 -34
  38. package/docs/quickstart.md +34 -35
  39. package/package.json +6 -2
@@ -0,0 +1,288 @@
1
+ import SwiftUI
2
+
3
+ struct OmniSearchView: View {
4
+ @ObservedObject var state: OmniSearchState
5
+ var onDismiss: () -> Void
6
+
7
+ @FocusState private var searchFocused: Bool
8
+
9
+ var body: some View {
10
+ VStack(spacing: 0) {
11
+ // Search field
12
+ HStack(spacing: 8) {
13
+ Image(systemName: "magnifyingglass")
14
+ .foregroundColor(Palette.textMuted)
15
+ .font(.system(size: 13))
16
+
17
+ TextField("Search windows, projects, sessions...", text: $state.query)
18
+ .textFieldStyle(.plain)
19
+ .font(Typo.mono(14))
20
+ .foregroundColor(Palette.text)
21
+ .focused($searchFocused)
22
+
23
+ if !state.query.isEmpty {
24
+ Button { state.query = "" } label: {
25
+ Image(systemName: "xmark.circle.fill")
26
+ .foregroundColor(Palette.textMuted)
27
+ .font(.system(size: 12))
28
+ }
29
+ .buttonStyle(.plain)
30
+ }
31
+ }
32
+ .padding(.horizontal, 14)
33
+ .padding(.vertical, 12)
34
+ .background(Palette.surface)
35
+
36
+ Rectangle()
37
+ .fill(Palette.border)
38
+ .frame(height: 0.5)
39
+
40
+ // Content
41
+ if state.query.isEmpty {
42
+ summaryView
43
+ } else if state.results.isEmpty {
44
+ emptyResults
45
+ } else {
46
+ resultsView
47
+ }
48
+ }
49
+ .frame(minWidth: 520, idealWidth: 520, maxWidth: 700, minHeight: 360, idealHeight: 480, maxHeight: 600)
50
+ .background(PanelBackground())
51
+ .preferredColorScheme(.dark)
52
+ .onAppear {
53
+ searchFocused = true
54
+ state.refreshSummary()
55
+ }
56
+ }
57
+
58
+ // MARK: - Results
59
+
60
+ private var resultsView: some View {
61
+ ScrollViewReader { proxy in
62
+ ScrollView {
63
+ LazyVStack(alignment: .leading, spacing: 2) {
64
+ var flatIndex = 0
65
+ ForEach(state.groupedResults, id: \.0) { group, items in
66
+ // Group header
67
+ Text(group.uppercased())
68
+ .font(Typo.caption(9))
69
+ .foregroundColor(Palette.textMuted)
70
+ .padding(.horizontal, 14)
71
+ .padding(.top, 8)
72
+ .padding(.bottom, 2)
73
+
74
+ ForEach(items) { item in
75
+ let idx = flatIndex
76
+ let _ = { flatIndex += 1 }()
77
+ resultRow(item, index: idx)
78
+ .id(item.id)
79
+ }
80
+ }
81
+ }
82
+ .padding(.vertical, 4)
83
+ }
84
+ .onChange(of: state.selectedIndex) { newVal in
85
+ if newVal < state.results.count {
86
+ let item = state.results[newVal]
87
+ withAnimation(.easeOut(duration: 0.1)) {
88
+ proxy.scrollTo(item.id, anchor: .center)
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ private func resultRow(_ item: OmniResult, index: Int) -> some View {
96
+ let isSelected = index == state.selectedIndex
97
+ return Button {
98
+ item.action()
99
+ onDismiss()
100
+ } label: {
101
+ HStack(spacing: 10) {
102
+ Image(systemName: item.icon)
103
+ .font(.system(size: 11, weight: .medium))
104
+ .foregroundColor(isSelected ? Palette.text : Palette.textDim)
105
+ .frame(width: 16)
106
+
107
+ VStack(alignment: .leading, spacing: 1) {
108
+ Text(item.title)
109
+ .font(Typo.mono(12))
110
+ .foregroundColor(isSelected ? Palette.text : Palette.textDim)
111
+ .lineLimit(1)
112
+
113
+ Text(item.subtitle)
114
+ .font(Typo.mono(10))
115
+ .foregroundColor(Palette.textMuted)
116
+ .lineLimit(1)
117
+ }
118
+
119
+ Spacer()
120
+
121
+ Text(item.kind.rawValue)
122
+ .font(Typo.mono(9))
123
+ .foregroundColor(Palette.textMuted)
124
+ .padding(.horizontal, 5)
125
+ .padding(.vertical, 2)
126
+ .background(
127
+ RoundedRectangle(cornerRadius: 3)
128
+ .fill(Palette.surface)
129
+ )
130
+ }
131
+ .padding(.horizontal, 14)
132
+ .padding(.vertical, 6)
133
+ .background(
134
+ RoundedRectangle(cornerRadius: 5)
135
+ .fill(isSelected ? Palette.surfaceHov : Color.clear)
136
+ )
137
+ .contentShape(Rectangle())
138
+ }
139
+ .buttonStyle(.plain)
140
+ }
141
+
142
+ // MARK: - Empty Results
143
+
144
+ private var emptyResults: some View {
145
+ VStack(spacing: 12) {
146
+ Spacer()
147
+ Image(systemName: "magnifyingglass")
148
+ .font(.system(size: 24, weight: .light))
149
+ .foregroundColor(Palette.textMuted)
150
+ Text("No results for \"\(state.query)\"")
151
+ .font(Typo.mono(12))
152
+ .foregroundColor(Palette.textDim)
153
+ Spacer()
154
+ }
155
+ }
156
+
157
+ // MARK: - Activity Summary
158
+
159
+ private var summaryView: some View {
160
+ ScrollView {
161
+ VStack(alignment: .leading, spacing: 14) {
162
+ if let summary = state.activitySummary {
163
+ // Windows by app
164
+ summarySection("WINDOWS", icon: "macwindow", count: summary.totalWindows) {
165
+ ForEach(summary.windowsByApp) { app in
166
+ HStack {
167
+ Text(app.appName)
168
+ .font(Typo.mono(11))
169
+ .foregroundColor(Palette.textDim)
170
+ .lineLimit(1)
171
+ Spacer()
172
+ Text("\(app.count)")
173
+ .font(Typo.monoBold(11))
174
+ .foregroundColor(Palette.text)
175
+ }
176
+ }
177
+ }
178
+
179
+ // Sessions
180
+ if !summary.sessions.isEmpty {
181
+ summarySection("TMUX SESSIONS", icon: "terminal", count: summary.sessions.count) {
182
+ ForEach(summary.sessions) { session in
183
+ HStack {
184
+ Circle()
185
+ .fill(session.attached ? Palette.running : Palette.textMuted)
186
+ .frame(width: 6, height: 6)
187
+ Text(session.name)
188
+ .font(Typo.mono(11))
189
+ .foregroundColor(Palette.textDim)
190
+ Spacer()
191
+ Text("\(session.paneCount) panes")
192
+ .font(Typo.mono(10))
193
+ .foregroundColor(Palette.textMuted)
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // Processes
200
+ if !summary.interestingProcesses.isEmpty {
201
+ summarySection("PROCESSES", icon: "gearshape", count: summary.interestingProcesses.count) {
202
+ ForEach(Array(summary.interestingProcesses.prefix(10).enumerated()), id: \.offset) { _, proc in
203
+ HStack {
204
+ Text(proc.comm)
205
+ .font(Typo.monoBold(11))
206
+ .foregroundColor(Palette.textDim)
207
+ if let cwd = proc.cwd {
208
+ Text(cwd.replacingOccurrences(of: NSHomeDirectory(), with: "~"))
209
+ .font(Typo.mono(10))
210
+ .foregroundColor(Palette.textMuted)
211
+ .lineLimit(1)
212
+ }
213
+ Spacer()
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // OCR info
220
+ if summary.ocrWindowCount > 0 {
221
+ HStack(spacing: 6) {
222
+ Image(systemName: "doc.text.magnifyingglass")
223
+ .font(.system(size: 10))
224
+ .foregroundColor(Palette.textMuted)
225
+ Text("OCR: \(summary.ocrWindowCount) windows scanned")
226
+ .font(Typo.mono(10))
227
+ .foregroundColor(Palette.textMuted)
228
+ if let t = summary.lastOcrScan {
229
+ Spacer()
230
+ Text(relativeTime(t))
231
+ .font(Typo.mono(9))
232
+ .foregroundColor(Palette.textMuted)
233
+ }
234
+ }
235
+ .padding(.horizontal, 14)
236
+ }
237
+ } else {
238
+ Text("Loading...")
239
+ .font(Typo.mono(11))
240
+ .foregroundColor(Palette.textMuted)
241
+ .padding(14)
242
+ }
243
+ }
244
+ .padding(.vertical, 10)
245
+ }
246
+ }
247
+
248
+ private func summarySection<Content: View>(
249
+ _ title: String,
250
+ icon: String,
251
+ count: Int,
252
+ @ViewBuilder content: () -> Content
253
+ ) -> some View {
254
+ VStack(alignment: .leading, spacing: 6) {
255
+ HStack(spacing: 6) {
256
+ Image(systemName: icon)
257
+ .font(.system(size: 10, weight: .medium))
258
+ .foregroundColor(Palette.textMuted)
259
+ Text(title)
260
+ .font(Typo.caption(9))
261
+ .foregroundColor(Palette.textMuted)
262
+ Text("\(count)")
263
+ .font(Typo.monoBold(9))
264
+ .foregroundColor(Palette.running)
265
+ .padding(.horizontal, 4)
266
+ .padding(.vertical, 1)
267
+ .background(
268
+ RoundedRectangle(cornerRadius: 3)
269
+ .fill(Palette.running.opacity(0.12))
270
+ )
271
+ Spacer()
272
+ }
273
+ .padding(.horizontal, 14)
274
+
275
+ VStack(spacing: 3) {
276
+ content()
277
+ }
278
+ .padding(.horizontal, 14)
279
+ }
280
+ }
281
+
282
+ private func relativeTime(_ date: Date) -> String {
283
+ let seconds = Int(Date().timeIntervalSince(date))
284
+ if seconds < 60 { return "\(seconds)s ago" }
285
+ if seconds < 3600 { return "\(seconds / 60)m ago" }
286
+ return "\(seconds / 3600)h ago"
287
+ }
288
+ }
@@ -0,0 +1,105 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ final class OmniSearchWindow {
5
+ static let shared = OmniSearchWindow()
6
+
7
+ private var panel: NSPanel?
8
+ private var keyMonitor: Any?
9
+ private var state: OmniSearchState?
10
+
11
+ var isVisible: Bool { panel?.isVisible ?? false }
12
+
13
+ func toggle() {
14
+ if isVisible {
15
+ dismiss()
16
+ } else {
17
+ show()
18
+ }
19
+ }
20
+
21
+ func show() {
22
+ if let p = panel, p.isVisible {
23
+ p.makeKeyAndOrderFront(nil)
24
+ NSApp.activate(ignoringOtherApps: true)
25
+ return
26
+ }
27
+
28
+ // Fresh state each time
29
+ let searchState = OmniSearchState()
30
+ state = searchState
31
+
32
+ let view = OmniSearchView(state: searchState) { [weak self] in
33
+ self?.dismiss()
34
+ }
35
+ .preferredColorScheme(.dark)
36
+
37
+ let hosting = NSHostingController(rootView: view)
38
+ hosting.preferredContentSize = NSSize(width: 520, height: 480)
39
+
40
+ let p = NSPanel(
41
+ contentRect: NSRect(x: 0, y: 0, width: 520, height: 480),
42
+ styleMask: [.titled, .closable, .resizable, .utilityWindow, .nonactivatingPanel],
43
+ backing: .buffered,
44
+ defer: false
45
+ )
46
+ p.contentViewController = hosting
47
+ p.title = "Omni Search"
48
+ p.titlebarAppearsTransparent = true
49
+ p.titleVisibility = .hidden
50
+ p.isMovableByWindowBackground = true
51
+ p.level = .floating
52
+ p.isOpaque = false
53
+ p.backgroundColor = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
54
+ p.hasShadow = true
55
+ p.appearance = NSAppearance(named: .darkAqua)
56
+ p.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
57
+ p.minSize = NSSize(width: 400, height: 300)
58
+ p.maxSize = NSSize(width: 700, height: 700)
59
+
60
+ // Center on screen
61
+ if let screen = NSScreen.main {
62
+ let visibleFrame = screen.visibleFrame
63
+ let x = visibleFrame.midX - 260
64
+ let y = visibleFrame.midY + 60 // slightly above center
65
+ p.setFrameOrigin(NSPoint(x: x, y: y))
66
+ }
67
+
68
+ p.makeKeyAndOrderFront(nil)
69
+ NSApp.activate(ignoringOtherApps: true)
70
+ panel = p
71
+
72
+ // Key monitor: Escape → dismiss, arrow keys → navigate, Enter → activate
73
+ keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
74
+ guard self?.panel?.isKeyWindow == true else { return event }
75
+
76
+ switch event.keyCode {
77
+ case 53: // Escape
78
+ self?.dismiss()
79
+ return nil
80
+ case 125: // ↓
81
+ self?.state?.moveSelection(1)
82
+ return nil
83
+ case 126: // ↑
84
+ self?.state?.moveSelection(-1)
85
+ return nil
86
+ case 36: // Enter
87
+ self?.state?.activateSelected()
88
+ self?.dismiss()
89
+ return nil
90
+ default:
91
+ return event
92
+ }
93
+ }
94
+ }
95
+
96
+ func dismiss() {
97
+ panel?.orderOut(nil)
98
+ panel = nil
99
+ state = nil
100
+ if let monitor = keyMonitor {
101
+ NSEvent.removeMonitor(monitor)
102
+ keyMonitor = nil
103
+ }
104
+ }
105
+ }
@@ -364,9 +364,19 @@ enum CommandBuilder {
364
364
  }
365
365
  ))
366
366
 
367
+ commands.append(PaletteCommand(
368
+ id: "app-windows-list",
369
+ title: "Windows List",
370
+ subtitle: "Browse all windows across displays",
371
+ icon: "rectangle.split.2x1",
372
+ category: .app,
373
+ badge: nil,
374
+ action: { CommandModeWindow.shared.show() }
375
+ ))
376
+
367
377
  commands.append(PaletteCommand(
368
378
  id: "app-screen-map",
369
- title: "Screen Map",
379
+ title: "Window Map",
370
380
  subtitle: "Visual window editor",
371
381
  icon: "rectangle.3.group",
372
382
  category: .app,
@@ -13,14 +13,14 @@ final class PermissionChecker: ObservableObject {
13
13
 
14
14
  var allGranted: Bool { accessibility && screenRecording }
15
15
 
16
- /// Check current permission state without prompting.
16
+ /// Check current permission state, prompting on first launch if not granted.
17
17
  func check() {
18
18
  let diag = DiagnosticLog.shared
19
19
 
20
20
  let ax = AXIsProcessTrusted()
21
21
  let sr = CGPreflightScreenCaptureAccess()
22
22
 
23
- // First check: log detailed identity info to help debug TCC issues
23
+ // First check: log identity info and prompt if needed
24
24
  if !hasLoggedInitial {
25
25
  hasLoggedInitial = true
26
26
  let bundleId = Bundle.main.bundleIdentifier ?? "<no bundle id>"
@@ -30,6 +30,16 @@ final class PermissionChecker: ObservableObject {
30
30
  diag.info("PermissionChecker: exec=\(execPath)")
31
31
  diag.info("AXIsProcessTrusted() → \(ax)")
32
32
  diag.info("CGPreflightScreenCaptureAccess() → \(sr)")
33
+
34
+ // Prompt for missing permissions on first check
35
+ if !ax {
36
+ requestAccessibility()
37
+ return
38
+ }
39
+ if !sr {
40
+ requestScreenRecording()
41
+ return
42
+ }
33
43
  }
34
44
 
35
45
  // Log on state changes
@@ -20,6 +20,32 @@ class Preferences: ObservableObject {
20
20
  didSet { UserDefaults.standard.set(mode.rawValue, forKey: "mode") }
21
21
  }
22
22
 
23
+ // MARK: - Search & OCR
24
+
25
+ @Published var ocrEnabled: Bool {
26
+ didSet { UserDefaults.standard.set(!ocrEnabled, forKey: "ocr.disabled") }
27
+ }
28
+
29
+ @Published var ocrQuickInterval: Double {
30
+ didSet { UserDefaults.standard.set(ocrQuickInterval, forKey: "ocr.interval") }
31
+ }
32
+
33
+ @Published var ocrDeepInterval: Double {
34
+ didSet { UserDefaults.standard.set(ocrDeepInterval, forKey: "ocr.deepInterval") }
35
+ }
36
+
37
+ @Published var ocrQuickLimit: Int {
38
+ didSet { UserDefaults.standard.set(ocrQuickLimit, forKey: "ocr.quickLimit") }
39
+ }
40
+
41
+ @Published var ocrDeepLimit: Int {
42
+ didSet { UserDefaults.standard.set(ocrDeepLimit, forKey: "ocr.deepLimit") }
43
+ }
44
+
45
+ @Published var ocrAccuracy: String {
46
+ didSet { UserDefaults.standard.set(ocrAccuracy, forKey: "ocr.accuracy") }
47
+ }
48
+
23
49
  init() {
24
50
  if let saved = UserDefaults.standard.string(forKey: "terminal"),
25
51
  let t = Terminal(rawValue: saved), t.isInstalled {
@@ -44,5 +70,23 @@ class Preferences: ObservableObject {
44
70
  } else {
45
71
  self.mode = .learning
46
72
  }
73
+
74
+ // Search & OCR
75
+ self.ocrEnabled = !UserDefaults.standard.bool(forKey: "ocr.disabled")
76
+
77
+ let savedInterval = UserDefaults.standard.double(forKey: "ocr.interval")
78
+ self.ocrQuickInterval = savedInterval > 0 ? savedInterval : 60
79
+
80
+ let savedDeep = UserDefaults.standard.double(forKey: "ocr.deepInterval")
81
+ self.ocrDeepInterval = savedDeep > 0 ? savedDeep : 7200
82
+
83
+ let savedQL = UserDefaults.standard.integer(forKey: "ocr.quickLimit")
84
+ self.ocrQuickLimit = savedQL > 0 ? savedQL : 5
85
+
86
+ let savedDL = UserDefaults.standard.integer(forKey: "ocr.deepLimit")
87
+ self.ocrDeepLimit = savedDL > 0 ? savedDL : 15
88
+
89
+ let savedAcc = UserDefaults.standard.string(forKey: "ocr.accuracy") ?? "accurate"
90
+ self.ocrAccuracy = savedAcc
47
91
  }
48
92
  }
@@ -1438,9 +1438,8 @@ final class ScreenMapController: ObservableObject {
1438
1438
  // Tiling mode intercepts keys before anything else
1439
1439
  if editor?.isTilingMode == true {
1440
1440
  switch keyCode {
1441
- case 53: // Escape cancel tiling mode
1442
- exitTilingMode()
1443
- flash("Tiling cancelled")
1441
+ case 53: // Escape always dismiss
1442
+ onDismiss?()
1444
1443
  return true
1445
1444
  case 123: // ← → left
1446
1445
  tileSelectedWindowInEditor(to: .left)
@@ -1516,8 +1515,8 @@ final class ScreenMapController: ObservableObject {
1516
1515
  // Search mode intercepts keys before normal handling
1517
1516
  if isSearchActive {
1518
1517
  switch keyCode {
1519
- case 53: // Escape close search
1520
- closeSearch()
1518
+ case 53: // Escape always dismiss
1519
+ onDismiss?()
1521
1520
  return true
1522
1521
  case 36: // Enter → select or focus
1523
1522
  if modifiers.contains(.command) {
@@ -1539,18 +1538,9 @@ final class ScreenMapController: ObservableObject {
1539
1538
  }
1540
1539
 
1541
1540
  switch keyCode {
1542
- case 53: // Escape
1543
- if editor?.isPreviewing == true {
1544
- endPreview()
1545
- }
1546
- if let ed = editor, ed.pendingEditCount > 0 {
1547
- ed.discardEdits()
1548
- diag.info("[ScreenMap] discarded edits")
1549
- flash("Edits discarded")
1550
- } else {
1551
- diag.info("[ScreenMap] exit")
1552
- onDismiss?()
1553
- }
1541
+ case 53: // Escape — always dismiss
1542
+ diag.info("[ScreenMap] exit")
1543
+ onDismiss?()
1554
1544
  return true
1555
1545
 
1556
1546
  case 36: // Enter
@@ -1972,6 +1972,9 @@ struct ScreenMapView: View {
1972
1972
 
1973
1973
  private func installKeyHandler() {
1974
1974
  eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp]) { event in
1975
+ // Only handle keys when our window is the key window
1976
+ guard let win = ScreenMapWindowController.shared.nsWindow,
1977
+ win.isKeyWindow else { return event }
1975
1978
  // Track space key for canvas drag-to-pan
1976
1979
  if event.keyCode == 49 && !controller.isSearchActive {
1977
1980
  if event.type == .keyDown && !event.isARepeat {