@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
package/README.md CHANGED
@@ -1,39 +1,68 @@
1
1
  <picture>
2
- <img alt="lattice" src="www/public/og.png" />
2
+ <img alt="lattices" src="site/public/og.png" />
3
3
  </picture>
4
4
 
5
- # lattice
5
+ # lattices
6
6
 
7
- Declarative tmux sessions for developers.
7
+ macOS workspace manager. Menu bar app, CLI, and a WebSocket API
8
+ so your AI agents can control the desktop.
8
9
 
9
- One command to create a named tmux session with your tools running. Auto-detects your stack, fully configurable with `.lattice.json`, and a native macOS menu bar app.
10
+ Window tiling, project discovery, workspace layers, on-screen OCR,
11
+ and optionally tmux-powered persistent sessions. 30 RPC methods
12
+ over WebSocket. One JSON config file.
10
13
 
11
14
  ## Install
12
15
 
13
16
  ```sh
14
- npm install -g lattice
17
+ npm install -g @arach/lattices
15
18
  ```
16
19
 
17
20
  ## Quick start
18
21
 
19
22
  ```sh
20
- cd my-project
21
- lattice
23
+ # Launch the menu bar app
24
+ lattices app
25
+
26
+ # Open the command palette from anywhere
27
+ # Cmd+Shift+M
28
+ ```
29
+
30
+ The app scans your projects, tiles windows, and gives you a command
31
+ palette for everything. Add tmux if you want persistent terminal
32
+ sessions:
33
+
34
+ ```sh
35
+ brew install tmux
36
+ cd my-project && lattices
22
37
  ```
23
38
 
24
- That's it. lattice creates a tmux session named after your project with Claude Code on the left and your dev server on the right. It detects your package manager and dev command automatically.
39
+ That creates a tmux session with Claude Code on the left and your
40
+ dev server on the right. Detach, close your laptop, come back later,
41
+ reattach. Everything is where you left it.
42
+
43
+ ## What it does
44
+
45
+ **Menu bar app** sits in your menu bar. Command palette, window tiling,
46
+ project discovery, workspace layers, OCR, and the daemon API. Works
47
+ with or without tmux.
48
+
49
+ **CLI** for tiling, session management, OCR queries, and tab groups.
25
50
 
26
- ## How it works
51
+ **Daemon API** on `ws://127.0.0.1:9399`. 30 RPC methods and 5
52
+ real-time events. Agents can discover projects, tile windows, launch
53
+ sessions, switch layers, and read on-screen text.
27
54
 
28
- 1. **Run `lattice`** in any project directory
29
- 2. A named tmux session is created with configured panes
30
- 3. Commands start running in each pane immediately
31
- 4. Detach with `Ctrl+b d`, reattach by running `lattice` again
32
- 5. Sessions persist in the background until you kill them
55
+ ```js
56
+ import { daemonCall } from '@arach/lattices/daemon-client'
57
+
58
+ const windows = await daemonCall('windows.list')
59
+ await daemonCall('session.launch', { path: '/Users/you/dev/frontend' })
60
+ await daemonCall('window.tile', { session: 'frontend-a1b2c3', position: 'left' })
61
+ ```
33
62
 
34
63
  ## Configuration
35
64
 
36
- Drop a `.lattice.json` in your project root:
65
+ Drop a `.lattices.json` in your project root:
37
66
 
38
67
  ```json
39
68
  {
@@ -46,111 +75,93 @@ Drop a `.lattice.json` in your project root:
46
75
  }
47
76
  ```
48
77
 
49
- ### Pane options
50
-
51
- | Field | Description |
52
- |--------|------------------------------------------|
53
- | `name` | Label for the pane (for your reference) |
54
- | `cmd` | Command to run in the pane |
55
- | `size` | Width % for the first pane (default: 60) |
56
-
57
- ### Session options
58
-
59
- | Field | Description |
60
- |-----------|-----------------------------------------------------------------------------|
61
- | `ensure` | Auto-restart exited commands on reattach |
62
- | `prefill` | Type exited commands into panes on reattach without running (you hit Enter) |
78
+ Or skip it. Without a config, lattices reads your `package.json` and
79
+ picks the right dev command automatically.
63
80
 
64
81
  ### Layouts
65
82
 
66
83
  ```
67
- 2 panes — side-by-side 3+ panes — main-vertical
68
-
69
- ┌──────────┬─────────┐ ┌──────────┬─────────┐
70
- │ claude │ server │ │ claude │ server │
71
- │ (60%) │ (40%) │ │ (60%) ├─────────┤
72
- └──────────┴─────────┘ │ │ tests │
73
- └──────────┴─────────┘
74
- ```
75
-
76
- ## Auto-detection
84
+ 2 panes 3+ panes
77
85
 
78
- Without a config file, lattice reads your `package.json` and picks the right command:
79
-
80
- - Checks `scripts.dev`, `scripts.start`, `scripts.serve`, `scripts.watch`
81
- - Detects package manager from lock files (pnpm, bun, yarn, npm)
82
- - Falls back to a shell if no dev command is found
83
-
84
- ## Menu bar app
85
-
86
- A macOS companion app for managing sessions without touching the terminal.
87
-
88
- ```sh
89
- lattice app # Launch (builds from source or downloads binary)
90
- lattice app build # Force rebuild from source
91
- lattice app quit # Stop the menu bar app
86
+ ┌──────────┬───────┐ ┌──────────┬───────┐
87
+ │ claude │server │ │ claude │server │
88
+ │ (60%) │(40%) │ │ (60%) ├───────┤
89
+ └──────────┴───────┘ │ │tests │
90
+ └──────────┴───────┘
92
91
  ```
93
92
 
94
- Features:
95
- - See all projects and their session status at a glance
96
- - Launch, attach, or detach sessions with a click
97
- - **Command palette** (`Cmd+Shift+M`): Raycast-style launcher for all actions — fuzzy search, keyboard navigation, instant access to projects, window tiling, and settings
98
- - Auto-scans your project directories
99
- - Built with SwiftUI, runs natively on macOS
100
-
101
- The app tries to compile from source first (requires Xcode CLI tools), falling back to a pre-built arm64 binary from GitHub releases.
93
+ ## Workspace layers
102
94
 
103
- ## Tab groups
95
+ Group projects into switchable contexts. `Cmd+Option+1` tiles your
96
+ frontend and API side by side. `Cmd+Option+2` switches to the mobile
97
+ stack. All sessions stay alive across switches.
104
98
 
105
- Bundle related projects as tabs within a single terminal window.
106
- Configure in `~/.lattice/workspace.json`:
99
+ Configure in `~/.lattices/workspace.json`:
107
100
 
108
101
  ```json
109
102
  {
110
- "name": "my-setup",
111
- "groups": [
103
+ "layers": [
112
104
  {
113
- "id": "talkie",
114
- "label": "Talkie",
115
- "tabs": [
116
- { "path": "/Users/you/dev/talkie-ios", "label": "iOS" },
117
- { "path": "/Users/you/dev/talkie-web", "label": "Website" },
118
- { "path": "/Users/you/dev/talkie-api", "label": "API" }
105
+ "id": "web", "label": "Web",
106
+ "projects": [
107
+ { "path": "/Users/you/dev/frontend", "tile": "left" },
108
+ { "path": "/Users/you/dev/api", "tile": "right" }
119
109
  ]
120
110
  }
121
111
  ]
122
112
  }
123
113
  ```
124
114
 
125
- Each tab gets its own tmux window with pane layout from its `.lattice.json`.
115
+ ## Tab groups
116
+
117
+ Bundle related repos as tabs in one session. Each tab gets its own
118
+ pane layout from its `.lattices.json`.
126
119
 
127
120
  ```sh
128
- lattice groups # List groups with status
129
- lattice group talkie # Launch or attach
130
- lattice tab talkie iOS # Switch to a tab
121
+ lattices group talkie # Launch iOS, macOS, Web, API as tabs
122
+ lattices tab talkie iOS # Switch to the iOS tab
131
123
  ```
132
124
 
133
- Groups can also be referenced in [workspace layers](https://lattice.dev/docs/layers) to tile a whole group into a screen position.
125
+ ## Screen OCR
126
+
127
+ The app reads text from visible windows using Apple Vision and indexes
128
+ it with FTS5 full-text search. Agents can search for error messages,
129
+ read terminal output, or find content across all your windows.
130
+
131
+ ```js
132
+ await daemonCall('ocr.scan')
133
+ const errors = await daemonCall('ocr.search', { query: 'error OR failed' })
134
+ ```
134
135
 
135
- ## CLI reference
136
+ ## CLI
136
137
 
137
138
  ```
138
- lattice Create or reattach to a session for the current project
139
- lattice init Generate a .lattice.json config
140
- lattice ls List active tmux sessions
141
- lattice kill [name] Kill a session (defaults to current project)
142
- lattice group [id] List tab groups or launch/attach a group
143
- lattice groups List all tab groups with status
144
- lattice tab <group> [tab] Switch tab within a group (by label or index)
145
- lattice app Launch the menu bar companion app
146
- lattice help Show help
139
+ lattices Create or reattach to session
140
+ lattices init Generate .lattices.json
141
+ lattices ls List active sessions
142
+ lattices kill [name] Kill a session
143
+ lattices tile <position> Tile frontmost window
144
+ lattices group [id] Launch or attach a tab group
145
+ lattices tab <group> [tab] Switch tab within a group
146
+ lattices ocr View current OCR snapshot
147
+ lattices ocr search <query> Search OCR history
148
+ lattices app Launch the menu bar app
149
+ lattices help Show help
147
150
  ```
148
151
 
149
152
  ## Requirements
150
153
 
151
- - **tmux** — `brew install tmux`
152
- - **Node.js** 18+
153
- - **macOS** (the CLI is macOS-only, the menu bar app requires arm64)
154
+ - macOS 13.0+
155
+ - Node.js 18+
156
+
157
+ ### Optional
158
+
159
+ - tmux for persistent terminal sessions (`brew install tmux`)
160
+ - Swift 5.9+ to build the menu bar app from source
161
+
162
+ ## Docs
163
+
164
+ Full documentation at [lattices.dev/docs](https://lattices.dev/docs/overview).
154
165
 
155
166
  ## License
156
167
 
@@ -0,0 +1,61 @@
1
+ import SwiftUI
2
+
3
+ /// A single action row with shortcut badge, label, optional icon, and hotkey hint.
4
+ struct ActionRow: View {
5
+ let shortcut: String
6
+ let label: String
7
+ var hotkey: String? = nil
8
+ var icon: String? = nil
9
+ var accentColor: Color = Palette.textDim
10
+ var action: () -> Void
11
+
12
+ @State private var isHovered = false
13
+
14
+ var body: some View {
15
+ Button(action: action) {
16
+ HStack(spacing: 10) {
17
+ // Shortcut badge
18
+ Text(shortcut)
19
+ .font(Typo.monoBold(10))
20
+ .foregroundColor(accentColor)
21
+ .frame(width: 18, height: 18)
22
+ .background(
23
+ RoundedRectangle(cornerRadius: 4)
24
+ .fill(accentColor.opacity(0.12))
25
+ )
26
+
27
+ // Icon
28
+ if let icon {
29
+ Image(systemName: icon)
30
+ .font(.system(size: 11, weight: .medium))
31
+ .foregroundColor(isHovered ? Palette.text : Palette.textDim)
32
+ .frame(width: 14)
33
+ }
34
+
35
+ // Label
36
+ Text(label)
37
+ .font(Typo.mono(12))
38
+ .foregroundColor(isHovered ? Palette.text : Palette.textDim)
39
+ .lineLimit(1)
40
+
41
+ Spacer()
42
+
43
+ // Hotkey
44
+ if let hotkey {
45
+ Text(hotkey)
46
+ .font(Typo.mono(10))
47
+ .foregroundColor(Palette.textMuted)
48
+ }
49
+ }
50
+ .padding(.horizontal, 10)
51
+ .padding(.vertical, 6)
52
+ .background(
53
+ RoundedRectangle(cornerRadius: 5)
54
+ .fill(isHovered ? Palette.surfaceHov : Color.clear)
55
+ )
56
+ .contentShape(Rectangle())
57
+ }
58
+ .buttonStyle(.plain)
59
+ .onHover { isHovered = $0 }
60
+ }
61
+ }
@@ -3,47 +3,8 @@ import SwiftUI
3
3
  @main
4
4
  struct LatticesApp: App {
5
5
  @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
6
- @StateObject private var scanner = ProjectScanner.shared
7
-
8
- /// 3×3 grid icon for the menu bar — L-shape bright, rest dim (template for auto light/dark)
9
- private static let menuBarIcon: NSImage = {
10
- let size: CGFloat = 18
11
- let img = NSImage(size: NSSize(width: size, height: size), flipped: false) { _ in
12
- let pad: CGFloat = 2
13
- let gap: CGFloat = 1.5
14
- let cellSize = (size - 2 * pad - 2 * gap) / 3
15
-
16
- // L-shape: left column + bottom row are solid, rest are dim
17
- let solidCells: Set<Int> = [0, 3, 6, 7, 8] // (0,0),(1,0),(2,0),(2,1),(2,2)
18
-
19
- for row in 0..<3 {
20
- for col in 0..<3 {
21
- let idx = row * 3 + col
22
- let x = pad + CGFloat(col) * (cellSize + gap)
23
- let y = pad + CGFloat(row) * (cellSize + gap)
24
- let rect = NSRect(x: x, y: y, width: cellSize, height: cellSize)
25
-
26
- if solidCells.contains(idx) {
27
- NSColor.black.setFill()
28
- } else {
29
- NSColor.black.withAlphaComponent(0.25).setFill()
30
- }
31
- let path = NSBezierPath(roundedRect: rect, xRadius: 0.8, yRadius: 0.8)
32
- path.fill()
33
- }
34
- }
35
- return true
36
- }
37
- img.isTemplate = true
38
- return img
39
- }()
40
6
 
41
7
  var body: some Scene {
42
- MenuBarExtra {
43
- MainView(scanner: scanner)
44
- } label: {
45
- Image(nsImage: Self.menuBarIcon)
46
- }
47
- .menuBarExtraStyle(.window)
8
+ Settings { EmptyView() }
48
9
  }
49
10
  }
@@ -1,18 +1,55 @@
1
1
  import AppKit
2
+ import SwiftUI
2
3
 
3
- /// Registers the global hotkey (Cmd+Shift+D) on launch.
4
- /// The menu bar itself is handled by SwiftUI's MenuBarExtra.
5
- class AppDelegate: NSObject, NSApplicationDelegate {
4
+ /// Manages the NSStatusItem (menu bar icon), left-click popover, and right-click context menu.
5
+ /// Replaces the previous SwiftUI MenuBarExtra approach for full click-event control.
6
+ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
7
+
8
+ private var statusItem: NSStatusItem!
9
+ private var popover: NSPopover?
10
+ private var contextMenu: NSMenu!
11
+
12
+ /// 3×3 grid icon for the menu bar — L-shape bright, rest dim (template for auto light/dark)
13
+ private static let menuBarIcon: NSImage = {
14
+ let size: CGFloat = 18
15
+ let img = NSImage(size: NSSize(width: size, height: size), flipped: true) { _ in
16
+ let pad: CGFloat = 2
17
+ let gap: CGFloat = 1.5
18
+ let cellSize = (size - 2 * pad - 2 * gap) / 3
19
+
20
+ let solidCells: Set<Int> = [0, 3, 6, 7, 8]
21
+
22
+ for row in 0..<3 {
23
+ for col in 0..<3 {
24
+ let idx = row * 3 + col
25
+ let x = pad + CGFloat(col) * (cellSize + gap)
26
+ let y = pad + CGFloat(row) * (cellSize + gap)
27
+ let rect = NSRect(x: x, y: y, width: cellSize, height: cellSize)
28
+
29
+ if solidCells.contains(idx) {
30
+ NSColor.black.setFill()
31
+ } else {
32
+ NSColor.black.withAlphaComponent(0.25).setFill()
33
+ }
34
+ let path = NSBezierPath(roundedRect: rect, xRadius: 0.8, yRadius: 0.8)
35
+ path.fill()
36
+ }
37
+ }
38
+ return true
39
+ }
40
+ img.isTemplate = true
41
+ return img
42
+ }()
6
43
 
7
44
  /// Toggle between .accessory (hidden from Dock/Cmd+Tab) and .regular (visible)
8
45
  /// based on whether any managed windows are open.
9
- /// Call this after showing or dismissing a window.
10
46
  static func updateActivationPolicy() {
11
47
  let hasVisibleWindow =
12
48
  CommandModeWindow.shared.isVisible ||
13
49
  CommandPaletteWindow.shared.isVisible ||
14
50
  MainWindow.shared.isVisible ||
15
- ScreenMapWindowController.shared.isVisible
51
+ ScreenMapWindowController.shared.isVisible ||
52
+ OmniSearchWindow.shared.isVisible
16
53
  let desired: NSApplication.ActivationPolicy = hasVisibleWindow ? .regular : .accessory
17
54
  if NSApp.activationPolicy() != desired {
18
55
  NSApp.setActivationPolicy(desired)
@@ -26,14 +63,29 @@ class AppDelegate: NSObject, NSApplicationDelegate {
26
63
  NSApp.setActivationPolicy(.accessory)
27
64
  NSApp.appearance = NSAppearance(named: .darkAqua)
28
65
 
29
- CommandPaletteWindow.shared.configure(scanner: ProjectScanner.shared)
66
+ // --- Status item ---
67
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
68
+ if let button = statusItem.button {
69
+ button.image = Self.menuBarIcon
70
+ button.action = #selector(statusItemClicked(_:))
71
+ button.sendAction(on: [.leftMouseUp, .rightMouseUp])
72
+ button.target = self
73
+ }
74
+
75
+ // --- Context menu (right-click) ---
76
+ contextMenu = buildContextMenu()
77
+
78
+ // --- Hotkey registration ---
79
+ let scanner = ProjectScanner.shared
80
+ CommandPaletteWindow.shared.configure(scanner: scanner)
30
81
 
31
- // Register all hotkeys via HotkeyStore (user-configurable bindings)
32
82
  let store = HotkeyStore.shared
33
83
  store.register(action: .palette) { CommandPaletteWindow.shared.toggle() }
34
84
  store.register(action: .screenMap) { ScreenMapWindowController.shared.toggle() }
35
85
  store.register(action: .bezel) { WindowBezel.showBezelForFrontmostWindow() }
36
86
  store.register(action: .cheatSheet) { CheatSheetHUD.shared.toggle() }
87
+ store.register(action: .desktopInventory) { CommandModeWindow.shared.toggle() }
88
+ store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
37
89
 
38
90
  // Layer-switching hotkeys
39
91
  let workspace = WorkspaceManager.shared
@@ -58,21 +110,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
58
110
  }
59
111
  store.register(action: .tileDistribute) { WindowTiler.distributeVisible() }
60
112
 
61
- // Style the MenuBarExtra panel when it appears
62
- NotificationCenter.default.addObserver(
63
- forName: NSWindow.didBecomeKeyNotification,
64
- object: nil,
65
- queue: .main
66
- ) { note in
67
- guard let panel = note.object as? NSPanel else { return }
68
- Self.stylePanel(panel)
69
- }
70
-
71
113
  // Check macOS permissions (Accessibility, Screen Recording)
72
114
  PermissionChecker.shared.check()
73
115
 
74
116
  // Start daemon services
117
+ OcrStore.shared.open()
75
118
  DesktopModel.shared.start()
119
+ OcrModel.shared.start()
76
120
  TmuxModel.shared.start()
77
121
  ProcessModel.shared.start()
78
122
  LatticesApi.setup()
@@ -93,12 +137,98 @@ class AppDelegate: NSObject, NSApplicationDelegate {
93
137
  }
94
138
  }
95
139
 
96
- private static func stylePanel(_ panel: NSPanel) {
97
- let bg = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
98
- panel.backgroundColor = bg
99
- panel.isOpaque = false
100
- panel.hasShadow = true
101
- panel.becomesKeyOnlyIfNeeded = false
102
- panel.invalidateShadow()
140
+ // MARK: - Status item click handler
141
+
142
+ @objc private func statusItemClicked(_ sender: Any?) {
143
+ guard let event = NSApp.currentEvent, let button = statusItem.button else { return }
144
+
145
+ if event.type == .rightMouseUp {
146
+ // Right-click → context menu
147
+ contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
148
+ } else {
149
+ // Left-click → toggle popover
150
+ if let shown = popover, shown.isShown {
151
+ shown.performClose(sender)
152
+ } else {
153
+ let p = makePopover()
154
+ p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
155
+ p.contentViewController?.view.window?.makeKey()
156
+ }
157
+ }
158
+ }
159
+
160
+ /// Dismiss the popover programmatically (e.g. from the pop-out button).
161
+ func dismissPopover() {
162
+ popover?.performClose(nil)
103
163
  }
164
+
165
+ /// Create a fresh popover each time so the SwiftUI view tree isn't kept alive
166
+ /// when the popover is closed — prevents continuous CPU usage from @Published updates.
167
+ private func makePopover() -> NSPopover {
168
+ let p = NSPopover()
169
+ p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
170
+ p.behavior = .transient
171
+ p.contentSize = NSSize(width: 380, height: 520)
172
+ p.appearance = NSAppearance(named: .darkAqua)
173
+ p.delegate = self
174
+ popover = p
175
+ return p
176
+ }
177
+
178
+ func popoverDidClose(_ notification: Notification) {
179
+ // Tear down the SwiftUI view tree so observed models stop driving re-renders
180
+ popover?.contentViewController = nil
181
+ popover = nil
182
+ }
183
+
184
+ // MARK: - Context menu
185
+
186
+ private func buildContextMenu() -> NSMenu {
187
+ let menu = NSMenu()
188
+
189
+ let actions: [(String, String, Selector)] = [
190
+ ("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
191
+ ("Screen Map", "", #selector(menuScreenMap)),
192
+ ("Desktop Inventory", "", #selector(menuDesktopInventory)),
193
+ ("Window Bezel", "", #selector(menuWindowBezel)),
194
+ ("Cheat Sheet", "", #selector(menuCheatSheet)),
195
+ ("Omni Search", "", #selector(menuOmniSearch)),
196
+ ]
197
+ for (title, shortcut, action) in actions {
198
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
199
+ item.target = self
200
+ if !shortcut.isEmpty {
201
+ // Display-only; the actual hotkey is global
202
+ }
203
+ menu.addItem(item)
204
+ }
205
+
206
+ menu.addItem(.separator())
207
+
208
+ let settings = NSMenuItem(title: "Settings…", action: #selector(menuSettings), keyEquivalent: ",")
209
+ settings.target = self
210
+ menu.addItem(settings)
211
+
212
+ let diag = NSMenuItem(title: "Diagnostics", action: #selector(menuDiagnostics), keyEquivalent: "")
213
+ diag.target = self
214
+ menu.addItem(diag)
215
+
216
+ menu.addItem(.separator())
217
+
218
+ let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
219
+ quit.target = self
220
+ menu.addItem(quit)
221
+
222
+ return menu
223
+ }
224
+
225
+ @objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
226
+ @objc private func menuScreenMap() { ScreenMapWindowController.shared.toggle() }
227
+ @objc private func menuDesktopInventory() { CommandModeWindow.shared.toggle() }
228
+ @objc private func menuWindowBezel() { WindowBezel.showBezelForFrontmostWindow() }
229
+ @objc private func menuCheatSheet() { CheatSheetHUD.shared.toggle() }
230
+ @objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
231
+ @objc private func menuSettings() { SettingsWindowController.shared.show() }
232
+ @objc private func menuDiagnostics() { DiagnosticWindow.shared.toggle() }
233
+ @objc private func menuQuit() { NSApp.terminate(nil) }
104
234
  }
@@ -231,6 +231,7 @@ struct CheatSheetView: View {
231
231
  shortcutRow(action: .screenMap)
232
232
  shortcutRow(action: .bezel)
233
233
  shortcutRow(action: .cheatSheet)
234
+ shortcutRow(action: .desktopInventory)
234
235
  }
235
236
  }
236
237