@lattices/cli 0.3.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 (74) hide show
  1. package/README.md +155 -0
  2. package/app/Lattices.app/Contents/Info.plist +24 -0
  3. package/app/Package.swift +13 -0
  4. package/app/Sources/AccessibilityTextExtractor.swift +111 -0
  5. package/app/Sources/ActionRow.swift +61 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +242 -0
  8. package/app/Sources/AppShellView.swift +62 -0
  9. package/app/Sources/AppTypeClassifier.swift +70 -0
  10. package/app/Sources/AppWindowShell.swift +63 -0
  11. package/app/Sources/CheatSheetHUD.swift +332 -0
  12. package/app/Sources/CommandModeState.swift +1362 -0
  13. package/app/Sources/CommandModeView.swift +1405 -0
  14. package/app/Sources/CommandModeWindow.swift +192 -0
  15. package/app/Sources/CommandPaletteView.swift +307 -0
  16. package/app/Sources/CommandPaletteWindow.swift +134 -0
  17. package/app/Sources/DaemonProtocol.swift +101 -0
  18. package/app/Sources/DaemonServer.swift +414 -0
  19. package/app/Sources/DesktopModel.swift +149 -0
  20. package/app/Sources/DesktopModelTypes.swift +71 -0
  21. package/app/Sources/DiagnosticLog.swift +271 -0
  22. package/app/Sources/EventBus.swift +30 -0
  23. package/app/Sources/HotkeyManager.swift +254 -0
  24. package/app/Sources/HotkeyStore.swift +338 -0
  25. package/app/Sources/InventoryManager.swift +35 -0
  26. package/app/Sources/InventoryPath.swift +43 -0
  27. package/app/Sources/KeyRecorderView.swift +210 -0
  28. package/app/Sources/LatticesApi.swift +1234 -0
  29. package/app/Sources/LayerBezel.swift +203 -0
  30. package/app/Sources/MainView.swift +479 -0
  31. package/app/Sources/MainWindow.swift +83 -0
  32. package/app/Sources/OcrModel.swift +430 -0
  33. package/app/Sources/OcrStore.swift +329 -0
  34. package/app/Sources/OmniSearchState.swift +283 -0
  35. package/app/Sources/OmniSearchView.swift +288 -0
  36. package/app/Sources/OmniSearchWindow.swift +105 -0
  37. package/app/Sources/OrphanRow.swift +129 -0
  38. package/app/Sources/PaletteCommand.swift +419 -0
  39. package/app/Sources/PermissionChecker.swift +125 -0
  40. package/app/Sources/Preferences.swift +99 -0
  41. package/app/Sources/ProcessModel.swift +199 -0
  42. package/app/Sources/ProcessQuery.swift +151 -0
  43. package/app/Sources/Project.swift +28 -0
  44. package/app/Sources/ProjectRow.swift +368 -0
  45. package/app/Sources/ProjectScanner.swift +128 -0
  46. package/app/Sources/ScreenMapState.swift +2387 -0
  47. package/app/Sources/ScreenMapView.swift +2820 -0
  48. package/app/Sources/ScreenMapWindowController.swift +89 -0
  49. package/app/Sources/SessionManager.swift +72 -0
  50. package/app/Sources/SettingsView.swift +1064 -0
  51. package/app/Sources/SettingsWindow.swift +20 -0
  52. package/app/Sources/TabGroupRow.swift +178 -0
  53. package/app/Sources/Terminal.swift +259 -0
  54. package/app/Sources/TerminalQuery.swift +156 -0
  55. package/app/Sources/TerminalSynthesizer.swift +200 -0
  56. package/app/Sources/Theme.swift +163 -0
  57. package/app/Sources/TilePickerView.swift +209 -0
  58. package/app/Sources/TmuxModel.swift +53 -0
  59. package/app/Sources/TmuxQuery.swift +81 -0
  60. package/app/Sources/WindowTiler.swift +1778 -0
  61. package/app/Sources/WorkspaceManager.swift +575 -0
  62. package/bin/client.js +4 -0
  63. package/bin/daemon-client.js +187 -0
  64. package/bin/lattices-app.js +221 -0
  65. package/bin/lattices.js +1551 -0
  66. package/docs/api.md +924 -0
  67. package/docs/app.md +297 -0
  68. package/docs/concepts.md +135 -0
  69. package/docs/config.md +245 -0
  70. package/docs/layers.md +410 -0
  71. package/docs/ocr.md +185 -0
  72. package/docs/overview.md +94 -0
  73. package/docs/quickstart.md +75 -0
  74. package/package.json +42 -0
@@ -0,0 +1,70 @@
1
+ import Foundation
2
+
3
+ enum AppType: String, CaseIterable {
4
+ case terminal
5
+ case editor
6
+ case browser
7
+ case chat
8
+ case media
9
+ case design
10
+ case system
11
+ case other
12
+
13
+ var label: String { rawValue }
14
+ }
15
+
16
+ enum AppTypeClassifier {
17
+ private static let nameMap: [String: AppType] = [
18
+ // Terminals
19
+ "iTerm2": .terminal, "Terminal": .terminal, "Alacritty": .terminal,
20
+ "kitty": .terminal, "Warp": .terminal, "Hyper": .terminal,
21
+ "WezTerm": .terminal, "Rio": .terminal, "Ghostty": .terminal,
22
+
23
+ // Editors / IDEs
24
+ "Xcode": .editor, "Code": .editor, "Visual Studio Code": .editor,
25
+ "Cursor": .editor, "Sublime Text": .editor, "TextEdit": .editor,
26
+ "Nova": .editor, "BBEdit": .editor, "Zed": .editor,
27
+ "IntelliJ IDEA": .editor, "WebStorm": .editor, "PyCharm": .editor,
28
+ "CLion": .editor, "GoLand": .editor, "RustRover": .editor,
29
+ "Android Studio": .editor, "Fleet": .editor, "Neovide": .editor,
30
+
31
+ // Browsers
32
+ "Safari": .browser, "Google Chrome": .browser, "Firefox": .browser,
33
+ "Arc": .browser, "Brave Browser": .browser, "Microsoft Edge": .browser,
34
+ "Orion": .browser, "Vivaldi": .browser, "Opera": .browser,
35
+ "Chrome": .browser, "Zen Browser": .browser,
36
+
37
+ // Chat / Communication
38
+ "Slack": .chat, "Discord": .chat, "Messages": .chat,
39
+ "Telegram": .chat, "WhatsApp": .chat, "Signal": .chat,
40
+ "Teams": .chat, "Microsoft Teams": .chat, "Zoom": .chat,
41
+ "FaceTime": .chat, "Skype": .chat,
42
+
43
+ // Media
44
+ "Spotify": .media, "Music": .media, "QuickTime Player": .media,
45
+ "VLC": .media, "IINA": .media, "Podcasts": .media,
46
+ "Photos": .media, "Preview": .media, "mpv": .media,
47
+
48
+ // Design
49
+ "Figma": .design, "Sketch": .design, "Pixelmator Pro": .design,
50
+ "Affinity Designer 2": .design, "Affinity Photo 2": .design,
51
+ "Adobe Photoshop": .design, "Adobe Illustrator": .design,
52
+ "Blender": .design, "OmniGraffle": .design,
53
+
54
+ // System
55
+ "Finder": .system, "System Preferences": .system, "System Settings": .system,
56
+ "Activity Monitor": .system, "Console": .system, "Disk Utility": .system,
57
+ "Keychain Access": .system,
58
+ ]
59
+
60
+ static func classify(_ appName: String) -> AppType {
61
+ if let exact = nameMap[appName] { return exact }
62
+ // Substring fallback
63
+ let lower = appName.lowercased()
64
+ if lower.contains("terminal") || lower.contains("term") { return .terminal }
65
+ if lower.contains("code") || lower.contains("studio") || lower.contains("edit") { return .editor }
66
+ if lower.contains("chrome") || lower.contains("firefox") || lower.contains("safari") || lower.contains("browser") { return .browser }
67
+ if lower.contains("slack") || lower.contains("discord") || lower.contains("chat") || lower.contains("teams") { return .chat }
68
+ return .other
69
+ }
70
+ }
@@ -0,0 +1,63 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ /// Shared factory for standalone NSWindow chrome.
5
+ /// Every managed window (Screen Map, Settings, Diagnostics, etc.) uses this
6
+ /// to get consistent title bar styling, dark appearance, and positioning.
7
+ struct AppWindowShell {
8
+
9
+ struct Config {
10
+ var title: String
11
+ var titleVisible: Bool = true
12
+ var initialSize: NSSize
13
+ var minSize: NSSize
14
+ var maxSize: NSSize
15
+ var miniaturizable: Bool = true
16
+ }
17
+
18
+ /// Create a styled NSWindow hosting a SwiftUI root view.
19
+ static func makeWindow<V: View>(config: Config, rootView: V) -> NSWindow {
20
+ let hosting = NSHostingView(rootView: rootView.preferredColorScheme(.dark))
21
+ hosting.frame = NSRect(origin: .zero, size: config.initialSize)
22
+
23
+ var styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable]
24
+ if config.miniaturizable { styleMask.insert(.miniaturizable) }
25
+
26
+ let w = NSWindow(
27
+ contentRect: NSRect(origin: .zero, size: config.initialSize),
28
+ styleMask: styleMask,
29
+ backing: .buffered,
30
+ defer: false
31
+ )
32
+ w.contentView = hosting
33
+ w.title = config.title
34
+ w.titlebarAppearsTransparent = true
35
+ w.titleVisibility = config.titleVisible ? .visible : .hidden
36
+ w.isReleasedWhenClosed = false
37
+ w.backgroundColor = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
38
+ w.appearance = NSAppearance(named: .darkAqua)
39
+ w.minSize = config.minSize
40
+ w.maxSize = config.maxSize
41
+ return w
42
+ }
43
+
44
+ /// Center the window on screen, nudged 8% above vertical center.
45
+ /// Clamps to 92% screen width / 85% screen height.
46
+ static func positionCentered(_ window: NSWindow) {
47
+ guard let screen = NSScreen.main else { return }
48
+ let frame = screen.visibleFrame
49
+ let size = window.frame.size
50
+ let w = min(size.width, frame.width * 0.92)
51
+ let h = min(size.height, frame.height * 0.85)
52
+ let x = frame.midX - w / 2
53
+ let y = frame.midY - h / 2 + (frame.height * 0.08)
54
+ window.setFrame(NSRect(x: x, y: y, width: w, height: h), display: true)
55
+ }
56
+
57
+ /// Bring the window to front and update activation policy.
58
+ static func present(_ window: NSWindow) {
59
+ window.makeKeyAndOrderFront(nil)
60
+ NSApp.activate(ignoringOtherApps: true)
61
+ AppDelegate.updateActivationPolicy()
62
+ }
63
+ }
@@ -0,0 +1,332 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ // MARK: - CheatSheetHUD (singleton window controller)
5
+
6
+ final class CheatSheetHUD {
7
+ static let shared = CheatSheetHUD()
8
+
9
+ private var panel: NSPanel?
10
+ private var localMonitor: Any?
11
+ private var globalMonitor: Any?
12
+
13
+ var isVisible: Bool { panel?.isVisible ?? false }
14
+
15
+ func toggle() {
16
+ if isVisible {
17
+ dismiss()
18
+ } else {
19
+ show()
20
+ }
21
+ }
22
+
23
+ func show() {
24
+ guard panel == nil else { return }
25
+
26
+ let view = CheatSheetView()
27
+ .preferredColorScheme(.dark)
28
+
29
+ let hosting = NSHostingView(rootView: view)
30
+
31
+ let p = NSPanel(
32
+ contentRect: NSRect(x: 0, y: 0, width: 520, height: 420),
33
+ styleMask: [.borderless, .nonactivatingPanel],
34
+ backing: .buffered,
35
+ defer: false
36
+ )
37
+ p.isOpaque = false
38
+ p.backgroundColor = .clear
39
+ p.level = .floating
40
+ p.hasShadow = true
41
+ p.hidesOnDeactivate = false
42
+ p.isReleasedWhenClosed = false
43
+ p.isMovableByWindowBackground = false
44
+ p.contentView = hosting
45
+
46
+ // Center on the screen containing the mouse cursor
47
+ let mouseLocation = NSEvent.mouseLocation
48
+ let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) ?? NSScreen.main ?? NSScreen.screens.first!
49
+ let screenFrame = screen.visibleFrame
50
+ let x = screenFrame.midX - 260
51
+ let y = screenFrame.midY - 210
52
+ p.setFrameOrigin(NSPoint(x: x, y: y))
53
+
54
+ p.alphaValue = 0
55
+ p.orderFrontRegardless()
56
+
57
+ NSAnimationContext.runAnimationGroup { ctx in
58
+ ctx.duration = 0.15
59
+ p.animator().alphaValue = 1.0
60
+ }
61
+
62
+ self.panel = p
63
+ installMonitors()
64
+ }
65
+
66
+ func dismiss() {
67
+ guard let p = panel else { return }
68
+ removeMonitors()
69
+
70
+ NSAnimationContext.runAnimationGroup({ ctx in
71
+ ctx.duration = 0.2
72
+ p.animator().alphaValue = 0
73
+ }) { [weak self] in
74
+ p.orderOut(nil)
75
+ self?.panel = nil
76
+ }
77
+ }
78
+
79
+ // MARK: - Event monitors
80
+
81
+ private func installMonitors() {
82
+ // Escape key dismisses (global — panel is non-activating so keys go to frontmost app)
83
+ localMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
84
+ if event.keyCode == 53 { // Escape
85
+ self?.dismiss()
86
+ }
87
+ }
88
+
89
+ // Click outside dismisses
90
+ globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in
91
+ self?.dismiss()
92
+ }
93
+ }
94
+
95
+ private func removeMonitors() {
96
+ if let m = localMonitor { NSEvent.removeMonitor(m); localMonitor = nil }
97
+ if let m = globalMonitor { NSEvent.removeMonitor(m); globalMonitor = nil }
98
+ }
99
+
100
+ }
101
+
102
+ // MARK: - CheatSheetView
103
+
104
+ struct CheatSheetView: View {
105
+ @ObservedObject private var hotkeyStore = HotkeyStore.shared
106
+
107
+ var body: some View {
108
+ VStack(spacing: 0) {
109
+ // Title
110
+ HStack {
111
+ Text("KEYBOARD SHORTCUTS")
112
+ .font(Typo.pixel(14))
113
+ .foregroundColor(Palette.textDim)
114
+ .tracking(1)
115
+ Spacer()
116
+ }
117
+ .padding(.horizontal, 20)
118
+ .padding(.top, 16)
119
+ .padding(.bottom, 10)
120
+
121
+ Rectangle().fill(Palette.border).frame(height: 0.5)
122
+
123
+ // Two-column body
124
+ HStack(alignment: .top, spacing: 20) {
125
+ // Left column: Tiling
126
+ tilingColumn
127
+
128
+ Rectangle().fill(Palette.border).frame(width: 0.5)
129
+
130
+ // Right column: App + tmux
131
+ VStack(alignment: .leading, spacing: 16) {
132
+ appColumn
133
+ Rectangle().fill(Palette.border).frame(height: 0.5)
134
+ tmuxColumn
135
+ }
136
+ .frame(maxWidth: .infinity, alignment: .leading)
137
+ }
138
+ .padding(.horizontal, 20)
139
+ .padding(.vertical, 14)
140
+
141
+ Spacer(minLength: 0)
142
+
143
+ Rectangle().fill(Palette.border).frame(height: 0.5)
144
+
145
+ // Footer
146
+ HStack {
147
+ Spacer()
148
+ Text("Press ESC to dismiss")
149
+ .font(Typo.caption(10))
150
+ .foregroundColor(Palette.textMuted)
151
+ Spacer()
152
+ }
153
+ .padding(.vertical, 8)
154
+ }
155
+ .frame(width: 520, height: 420)
156
+ .background(
157
+ RoundedRectangle(cornerRadius: 12)
158
+ .fill(Palette.bg)
159
+ .overlay(
160
+ RoundedRectangle(cornerRadius: 12)
161
+ .strokeBorder(Palette.borderLit, lineWidth: 0.5)
162
+ )
163
+ )
164
+ .clipShape(RoundedRectangle(cornerRadius: 12))
165
+ }
166
+
167
+ // MARK: - Tiling Column
168
+
169
+ private var tilingColumn: some View {
170
+ VStack(alignment: .leading, spacing: 10) {
171
+ columnHeader("Tiling")
172
+
173
+ // 3x3 grid
174
+ VStack(spacing: 2) {
175
+ HStack(spacing: 2) {
176
+ tileCell(action: .tileTopLeft, label: "TL")
177
+ tileCell(action: .tileTop, label: "Top")
178
+ tileCell(action: .tileTopRight, label: "TR")
179
+ }
180
+ HStack(spacing: 2) {
181
+ tileCell(action: .tileLeft, label: "Left")
182
+ tileCell(action: .tileMaximize, label: "Max")
183
+ tileCell(action: .tileRight, label: "Right")
184
+ }
185
+ HStack(spacing: 2) {
186
+ tileCell(action: .tileBottomLeft, label: "BL")
187
+ tileCell(action: .tileBottom, label: "Bot")
188
+ tileCell(action: .tileBottomRight, label: "BR")
189
+ }
190
+ }
191
+ .padding(6)
192
+ .background(
193
+ RoundedRectangle(cornerRadius: 6)
194
+ .fill(Color.black.opacity(0.25))
195
+ .overlay(
196
+ RoundedRectangle(cornerRadius: 6)
197
+ .strokeBorder(Palette.border, lineWidth: 0.5)
198
+ )
199
+ )
200
+
201
+ // Thirds row
202
+ HStack(spacing: 2) {
203
+ tileCell(action: .tileLeftThird, label: "\u{2153}L")
204
+ tileCell(action: .tileCenterThird, label: "\u{2153}C")
205
+ tileCell(action: .tileRightThird, label: "\u{2153}R")
206
+ }
207
+ .padding(6)
208
+ .background(
209
+ RoundedRectangle(cornerRadius: 6)
210
+ .fill(Color.black.opacity(0.25))
211
+ .overlay(
212
+ RoundedRectangle(cornerRadius: 6)
213
+ .strokeBorder(Palette.border, lineWidth: 0.5)
214
+ )
215
+ )
216
+
217
+ // Center + Distribute
218
+ shortcutRow(action: .tileCenter)
219
+ shortcutRow(action: .tileDistribute)
220
+ }
221
+ .frame(maxWidth: .infinity, alignment: .leading)
222
+ }
223
+
224
+ // MARK: - App Column
225
+
226
+ private var appColumn: some View {
227
+ VStack(alignment: .leading, spacing: 8) {
228
+ columnHeader("App")
229
+
230
+ shortcutRow(action: .palette)
231
+ shortcutRow(action: .screenMap)
232
+ shortcutRow(action: .bezel)
233
+ shortcutRow(action: .cheatSheet)
234
+ shortcutRow(action: .desktopInventory)
235
+ }
236
+ }
237
+
238
+ // MARK: - tmux Column
239
+
240
+ private var tmuxColumn: some View {
241
+ VStack(alignment: .leading, spacing: 6) {
242
+ columnHeader("Inside tmux")
243
+
244
+ tmuxRow("Detach", keys: ["Ctrl+B", "D"])
245
+ tmuxRow("Kill pane", keys: ["Ctrl+B", "X"])
246
+ tmuxRow("Pane left", keys: ["Ctrl+B", "\u{2190}"])
247
+ tmuxRow("Pane right", keys: ["Ctrl+B", "\u{2192}"])
248
+ tmuxRow("Zoom toggle", keys: ["Ctrl+B", "Z"])
249
+ tmuxRow("Scroll mode", keys: ["Ctrl+B", "["])
250
+ }
251
+ }
252
+
253
+ // MARK: - Shared components
254
+
255
+ private func columnHeader(_ title: String) -> some View {
256
+ Text(title.uppercased())
257
+ .font(Typo.pixel(12))
258
+ .foregroundColor(Palette.textDim)
259
+ .tracking(1)
260
+ }
261
+
262
+ private func tileCell(action: HotkeyAction, label: String) -> some View {
263
+ let binding = hotkeyStore.bindings[action]
264
+ let badgeText = binding?.displayParts.last ?? ""
265
+
266
+ return VStack(spacing: 3) {
267
+ Text(label)
268
+ .font(Typo.caption(9))
269
+ .foregroundColor(Palette.textDim)
270
+ Text(badgeText)
271
+ .font(Typo.geistMonoBold(9))
272
+ .foregroundColor(Palette.text)
273
+ }
274
+ .frame(maxWidth: .infinity)
275
+ .frame(height: 38)
276
+ .background(
277
+ RoundedRectangle(cornerRadius: 4)
278
+ .fill(Palette.surface)
279
+ .overlay(
280
+ RoundedRectangle(cornerRadius: 4)
281
+ .strokeBorder(Palette.border, lineWidth: 0.5)
282
+ )
283
+ )
284
+ }
285
+
286
+ private func shortcutRow(action: HotkeyAction) -> some View {
287
+ let binding = hotkeyStore.bindings[action]
288
+ return HStack(spacing: 8) {
289
+ if let parts = binding?.displayParts {
290
+ HStack(spacing: 3) {
291
+ ForEach(parts, id: \.self) { part in
292
+ keyBadge(part)
293
+ }
294
+ }
295
+ }
296
+ Text(action.label)
297
+ .font(Typo.caption(11))
298
+ .foregroundColor(Palette.textDim)
299
+ Spacer()
300
+ }
301
+ }
302
+
303
+ private func tmuxRow(_ label: String, keys: [String]) -> some View {
304
+ HStack(spacing: 8) {
305
+ HStack(spacing: 3) {
306
+ ForEach(keys, id: \.self) { key in
307
+ keyBadge(key)
308
+ }
309
+ }
310
+ Text(label)
311
+ .font(Typo.caption(11))
312
+ .foregroundColor(Palette.textDim)
313
+ Spacer()
314
+ }
315
+ }
316
+
317
+ private func keyBadge(_ key: String) -> some View {
318
+ Text(key)
319
+ .font(Typo.geistMonoBold(10))
320
+ .foregroundColor(Palette.text)
321
+ .padding(.horizontal, 6)
322
+ .padding(.vertical, 3)
323
+ .background(
324
+ RoundedRectangle(cornerRadius: 3)
325
+ .fill(Palette.surface)
326
+ .overlay(
327
+ RoundedRectangle(cornerRadius: 3)
328
+ .strokeBorder(Palette.border, lineWidth: 0.5)
329
+ )
330
+ )
331
+ }
332
+ }