@arach/lattices 0.2.0 → 0.6.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.
- package/LICENSE +21 -0
- package/README.md +172 -86
- package/apps/mac/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
- package/apps/mac/Lattices.entitlements +21 -0
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Resources/tap.wav +0 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/assistant-intelligence.ts +912 -0
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +17 -0
- package/bin/cua.ts +26 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +96 -0
- package/bin/handsoff-worker.ts +531 -0
- package/bin/infer.ts +424 -0
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +655 -0
- package/bin/lattices-build +125 -0
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +362 -0
- package/bin/lattices.ts +3260 -0
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +233 -0
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +1041 -47
- package/docs/app.md +96 -13
- package/docs/assistant-knowledge.md +130 -0
- package/docs/companion-deck.md +209 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/concepts.md +13 -12
- package/docs/config.md +83 -10
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +176 -28
- package/docs/mouse-gestures.md +244 -0
- package/docs/ocr.md +21 -9
- package/docs/overview.md +42 -23
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +382 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +8 -12
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/release.md +172 -0
- package/docs/repo-structure.md +100 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +224 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice-error-model.md +73 -0
- package/docs/voice.md +221 -0
- package/package.json +69 -16
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
- package/app/Lattices.app/Contents/Info.plist +0 -24
- package/app/Package.swift +0 -13
- package/app/Sources/ActionRow.swift +0 -61
- package/app/Sources/App.swift +0 -10
- package/app/Sources/AppDelegate.swift +0 -234
- package/app/Sources/AppShellView.swift +0 -62
- package/app/Sources/AppTypeClassifier.swift +0 -70
- package/app/Sources/AppWindowShell.swift +0 -63
- package/app/Sources/CheatSheetHUD.swift +0 -332
- package/app/Sources/CommandModeState.swift +0 -1362
- package/app/Sources/CommandModeView.swift +0 -1405
- package/app/Sources/CommandModeWindow.swift +0 -192
- package/app/Sources/CommandPaletteView.swift +0 -307
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/DaemonProtocol.swift +0 -101
- package/app/Sources/DaemonServer.swift +0 -414
- package/app/Sources/DesktopModel.swift +0 -121
- package/app/Sources/DesktopModelTypes.swift +0 -71
- package/app/Sources/DiagnosticLog.swift +0 -271
- package/app/Sources/EventBus.swift +0 -30
- package/app/Sources/HotkeyManager.swift +0 -250
- package/app/Sources/HotkeyStore.swift +0 -338
- package/app/Sources/InventoryManager.swift +0 -35
- package/app/Sources/InventoryPath.swift +0 -43
- package/app/Sources/KeyRecorderView.swift +0 -210
- package/app/Sources/LatticesApi.swift +0 -1125
- package/app/Sources/MainView.swift +0 -467
- package/app/Sources/MainWindow.swift +0 -83
- package/app/Sources/OcrModel.swift +0 -309
- package/app/Sources/OcrStore.swift +0 -295
- package/app/Sources/OmniSearchState.swift +0 -283
- package/app/Sources/OmniSearchView.swift +0 -288
- package/app/Sources/OmniSearchWindow.swift +0 -105
- package/app/Sources/OrphanRow.swift +0 -129
- package/app/Sources/PaletteCommand.swift +0 -419
- package/app/Sources/PermissionChecker.swift +0 -125
- package/app/Sources/Preferences.swift +0 -92
- package/app/Sources/ProcessModel.swift +0 -199
- package/app/Sources/ProcessQuery.swift +0 -151
- package/app/Sources/Project.swift +0 -28
- package/app/Sources/ProjectRow.swift +0 -368
- package/app/Sources/ProjectScanner.swift +0 -121
- package/app/Sources/ScreenMapState.swift +0 -2387
- package/app/Sources/ScreenMapView.swift +0 -2820
- package/app/Sources/ScreenMapWindowController.swift +0 -89
- package/app/Sources/SessionManager.swift +0 -72
- package/app/Sources/SettingsView.swift +0 -1053
- package/app/Sources/SettingsWindow.swift +0 -20
- package/app/Sources/TabGroupRow.swift +0 -178
- package/app/Sources/Terminal.swift +0 -259
- package/app/Sources/TerminalQuery.swift +0 -156
- package/app/Sources/TerminalSynthesizer.swift +0 -200
- package/app/Sources/Theme.swift +0 -163
- package/app/Sources/TilePickerView.swift +0 -209
- package/app/Sources/TmuxModel.swift +0 -53
- package/app/Sources/TmuxQuery.swift +0 -81
- package/app/Sources/WindowTiler.swift +0 -1755
- package/app/Sources/WorkspaceManager.swift +0 -434
- package/bin/lattices-app.js +0 -221
- package/bin/lattices.js +0 -1418
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import Combine
|
|
3
|
-
import Foundation
|
|
4
|
-
|
|
5
|
-
// MARK: - Result Types
|
|
6
|
-
|
|
7
|
-
enum OmniResultKind: String {
|
|
8
|
-
case window
|
|
9
|
-
case project
|
|
10
|
-
case session
|
|
11
|
-
case process
|
|
12
|
-
case ocrContent
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
struct OmniResult: Identifiable {
|
|
16
|
-
let id = UUID()
|
|
17
|
-
let kind: OmniResultKind
|
|
18
|
-
let title: String
|
|
19
|
-
let subtitle: String
|
|
20
|
-
let icon: String
|
|
21
|
-
let score: Int // higher = better match
|
|
22
|
-
let action: () -> Void
|
|
23
|
-
|
|
24
|
-
/// Group label for display
|
|
25
|
-
var groupLabel: String {
|
|
26
|
-
switch kind {
|
|
27
|
-
case .window: return "Windows"
|
|
28
|
-
case .project: return "Projects"
|
|
29
|
-
case .session: return "Sessions"
|
|
30
|
-
case .process: return "Processes"
|
|
31
|
-
case .ocrContent: return "Screen Text"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// MARK: - Activity Summary
|
|
37
|
-
|
|
38
|
-
struct ActivitySummary {
|
|
39
|
-
struct AppWindowCount: Identifiable {
|
|
40
|
-
let id: String
|
|
41
|
-
let appName: String
|
|
42
|
-
let count: Int
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
struct SessionInfo: Identifiable {
|
|
46
|
-
let id: String
|
|
47
|
-
let name: String
|
|
48
|
-
let paneCount: Int
|
|
49
|
-
let attached: Bool
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let windowsByApp: [AppWindowCount]
|
|
53
|
-
let totalWindows: Int
|
|
54
|
-
let sessions: [SessionInfo]
|
|
55
|
-
let interestingProcesses: [ProcessEntry]
|
|
56
|
-
let lastOcrScan: Date?
|
|
57
|
-
let ocrWindowCount: Int
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// MARK: - State
|
|
61
|
-
|
|
62
|
-
final class OmniSearchState: ObservableObject {
|
|
63
|
-
@Published var query: String = ""
|
|
64
|
-
@Published var results: [OmniResult] = []
|
|
65
|
-
@Published var selectedIndex: Int = 0
|
|
66
|
-
@Published var activitySummary: ActivitySummary?
|
|
67
|
-
|
|
68
|
-
private var cancellables = Set<AnyCancellable>()
|
|
69
|
-
private var debounceTimer: AnyCancellable?
|
|
70
|
-
|
|
71
|
-
init() {
|
|
72
|
-
// Debounce search by 150ms
|
|
73
|
-
debounceTimer = $query
|
|
74
|
-
.debounce(for: .milliseconds(150), scheduler: RunLoop.main)
|
|
75
|
-
.sink { [weak self] q in
|
|
76
|
-
if q.isEmpty {
|
|
77
|
-
self?.results = []
|
|
78
|
-
self?.refreshSummary()
|
|
79
|
-
} else {
|
|
80
|
-
self?.search(q)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
refreshSummary()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// MARK: - Search
|
|
88
|
-
|
|
89
|
-
private func search(_ query: String) {
|
|
90
|
-
let q = query.lowercased()
|
|
91
|
-
var all: [OmniResult] = []
|
|
92
|
-
|
|
93
|
-
// Windows
|
|
94
|
-
let desktop = DesktopModel.shared
|
|
95
|
-
for win in desktop.allWindows() {
|
|
96
|
-
let score = scoreMatch(q, against: [win.app, win.title])
|
|
97
|
-
if score > 0 {
|
|
98
|
-
let wid = win.wid
|
|
99
|
-
let pid = win.pid
|
|
100
|
-
all.append(OmniResult(
|
|
101
|
-
kind: .window,
|
|
102
|
-
title: win.app,
|
|
103
|
-
subtitle: win.title.isEmpty ? "Window \(win.wid)" : win.title,
|
|
104
|
-
icon: "macwindow",
|
|
105
|
-
score: score
|
|
106
|
-
) {
|
|
107
|
-
WindowTiler.focusWindow(wid: wid, pid: pid)
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Projects
|
|
113
|
-
let scanner = ProjectScanner.shared
|
|
114
|
-
for project in scanner.projects {
|
|
115
|
-
let score = scoreMatch(q, against: [project.name, project.path])
|
|
116
|
-
if score > 0 {
|
|
117
|
-
let proj = project
|
|
118
|
-
all.append(OmniResult(
|
|
119
|
-
kind: .project,
|
|
120
|
-
title: project.name,
|
|
121
|
-
subtitle: project.path,
|
|
122
|
-
icon: "folder",
|
|
123
|
-
score: score
|
|
124
|
-
) {
|
|
125
|
-
SessionManager.launch(project: proj)
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Tmux Sessions
|
|
131
|
-
let tmux = TmuxModel.shared
|
|
132
|
-
for session in tmux.sessions {
|
|
133
|
-
let paneCommands = session.panes.map(\.currentCommand)
|
|
134
|
-
let score = scoreMatch(q, against: [session.name] + paneCommands)
|
|
135
|
-
if score > 0 {
|
|
136
|
-
let name = session.name
|
|
137
|
-
all.append(OmniResult(
|
|
138
|
-
kind: .session,
|
|
139
|
-
title: session.name,
|
|
140
|
-
subtitle: "\(session.windowCount) windows, \(session.panes.count) panes\(session.attached ? " (attached)" : "")",
|
|
141
|
-
icon: "terminal",
|
|
142
|
-
score: score
|
|
143
|
-
) {
|
|
144
|
-
let terminal = Preferences.shared.terminal
|
|
145
|
-
terminal.focusOrAttach(session: name)
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Processes
|
|
151
|
-
let processes = ProcessModel.shared
|
|
152
|
-
for proc in processes.interesting {
|
|
153
|
-
let score = scoreMatch(q, against: [proc.comm, proc.args, proc.cwd ?? ""])
|
|
154
|
-
if score > 0 {
|
|
155
|
-
all.append(OmniResult(
|
|
156
|
-
kind: .process,
|
|
157
|
-
title: proc.comm,
|
|
158
|
-
subtitle: proc.cwd ?? proc.args,
|
|
159
|
-
icon: "gearshape",
|
|
160
|
-
score: score
|
|
161
|
-
) {
|
|
162
|
-
// No direct action for processes — just informational
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// OCR content
|
|
168
|
-
let ocr = OcrModel.shared
|
|
169
|
-
for (_, result) in ocr.results {
|
|
170
|
-
let ocrScore = scoreOcr(q, fullText: result.fullText)
|
|
171
|
-
if ocrScore > 0 {
|
|
172
|
-
let wid = result.wid
|
|
173
|
-
let pid = desktop.windows[wid]?.pid ?? 0
|
|
174
|
-
// Find matching line for subtitle
|
|
175
|
-
let matchLine = result.texts
|
|
176
|
-
.first { $0.text.lowercased().contains(q) }?
|
|
177
|
-
.text ?? String(result.fullText.prefix(80))
|
|
178
|
-
all.append(OmniResult(
|
|
179
|
-
kind: .ocrContent,
|
|
180
|
-
title: "\(result.app) — \(result.title)",
|
|
181
|
-
subtitle: matchLine,
|
|
182
|
-
icon: "doc.text.magnifyingglass",
|
|
183
|
-
score: ocrScore
|
|
184
|
-
) {
|
|
185
|
-
WindowTiler.focusWindow(wid: wid, pid: pid)
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Sort by score descending
|
|
191
|
-
all.sort { $0.score > $1.score }
|
|
192
|
-
|
|
193
|
-
results = all
|
|
194
|
-
selectedIndex = 0
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// MARK: - Scoring
|
|
198
|
-
|
|
199
|
-
private func scoreMatch(_ query: String, against fields: [String]) -> Int {
|
|
200
|
-
var best = 0
|
|
201
|
-
for field in fields {
|
|
202
|
-
let lower = field.lowercased()
|
|
203
|
-
if lower == query {
|
|
204
|
-
best = max(best, 100) // exact
|
|
205
|
-
} else if lower.hasPrefix(query) {
|
|
206
|
-
best = max(best, 80) // prefix
|
|
207
|
-
} else if lower.contains(query) {
|
|
208
|
-
best = max(best, 60) // contains
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return best
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private func scoreOcr(_ query: String, fullText: String) -> Int {
|
|
215
|
-
let lower = fullText.lowercased()
|
|
216
|
-
if lower.contains(query) { return 40 }
|
|
217
|
-
return 0
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// MARK: - Navigation
|
|
221
|
-
|
|
222
|
-
func moveSelection(_ delta: Int) {
|
|
223
|
-
guard !results.isEmpty else { return }
|
|
224
|
-
selectedIndex = max(0, min(results.count - 1, selectedIndex + delta))
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
func activateSelected() {
|
|
228
|
-
guard selectedIndex >= 0, selectedIndex < results.count else { return }
|
|
229
|
-
results[selectedIndex].action()
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// MARK: - Activity Summary
|
|
233
|
-
|
|
234
|
-
func refreshSummary() {
|
|
235
|
-
let desktop = DesktopModel.shared
|
|
236
|
-
let windows = desktop.allWindows()
|
|
237
|
-
|
|
238
|
-
// Group by app
|
|
239
|
-
var appCounts: [String: Int] = [:]
|
|
240
|
-
for win in windows {
|
|
241
|
-
appCounts[win.app, default: 0] += 1
|
|
242
|
-
}
|
|
243
|
-
let windowsByApp = appCounts
|
|
244
|
-
.sorted { $0.value > $1.value }
|
|
245
|
-
.map { ActivitySummary.AppWindowCount(id: $0.key, appName: $0.key, count: $0.value) }
|
|
246
|
-
|
|
247
|
-
// Sessions
|
|
248
|
-
let sessions = TmuxModel.shared.sessions.map {
|
|
249
|
-
ActivitySummary.SessionInfo(
|
|
250
|
-
id: $0.id,
|
|
251
|
-
name: $0.name,
|
|
252
|
-
paneCount: $0.panes.count,
|
|
253
|
-
attached: $0.attached
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Processes
|
|
258
|
-
let procs = ProcessModel.shared.interesting
|
|
259
|
-
|
|
260
|
-
// OCR info
|
|
261
|
-
let ocrResults = OcrModel.shared.results
|
|
262
|
-
let lastScan: Date? = ocrResults.values.map(\.timestamp).max()
|
|
263
|
-
|
|
264
|
-
activitySummary = ActivitySummary(
|
|
265
|
-
windowsByApp: windowsByApp,
|
|
266
|
-
totalWindows: windows.count,
|
|
267
|
-
sessions: sessions,
|
|
268
|
-
interestingProcesses: procs,
|
|
269
|
-
lastOcrScan: lastScan,
|
|
270
|
-
ocrWindowCount: ocrResults.count
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/// Grouped results for display
|
|
275
|
-
var groupedResults: [(String, [OmniResult])] {
|
|
276
|
-
let groups = Dictionary(grouping: results) { $0.groupLabel }
|
|
277
|
-
let order: [String] = ["Windows", "Projects", "Sessions", "Processes", "Screen Text"]
|
|
278
|
-
return order.compactMap { key in
|
|
279
|
-
guard let items = groups[key], !items.isEmpty else { return nil }
|
|
280
|
-
return (key, items)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
@@ -1,288 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
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
|
-
}
|