@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.
- package/README.md +101 -90
- package/app/Sources/ActionRow.swift +61 -0
- package/app/Sources/App.swift +1 -40
- package/app/Sources/AppDelegate.swift +154 -24
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CommandModeState.swift +40 -19
- package/app/Sources/CommandModeView.swift +27 -2
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DiagnosticLog.swift +19 -1
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HotkeyManager.swift +1 -0
- package/app/Sources/HotkeyStore.swift +9 -1
- package/app/Sources/LatticesApi.swift +210 -0
- package/app/Sources/MainView.swift +46 -86
- package/app/Sources/MainWindow.swift +13 -0
- package/app/Sources/OcrModel.swift +309 -0
- package/app/Sources/OcrStore.swift +295 -0
- package/app/Sources/OmniSearchState.swift +283 -0
- package/app/Sources/OmniSearchView.swift +288 -0
- package/app/Sources/OmniSearchWindow.swift +105 -0
- package/app/Sources/PaletteCommand.swift +11 -1
- package/app/Sources/PermissionChecker.swift +12 -2
- package/app/Sources/Preferences.swift +44 -0
- package/app/Sources/ScreenMapState.swift +7 -17
- package/app/Sources/ScreenMapView.swift +3 -0
- package/app/Sources/SettingsView.swift +534 -122
- package/app/Sources/Theme.swift +39 -0
- package/app/Sources/WindowTiler.swift +59 -56
- package/bin/lattices-app.js +23 -7
- package/bin/lattices.js +123 -0
- package/docs/api.md +390 -249
- package/docs/app.md +75 -28
- package/docs/concepts.md +45 -136
- package/docs/config.md +8 -7
- package/docs/layers.md +16 -18
- package/docs/ocr.md +185 -0
- package/docs/overview.md +39 -34
- package/docs/quickstart.md +34 -35
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,39 +1,68 @@
|
|
|
1
1
|
<picture>
|
|
2
|
-
<img alt="
|
|
2
|
+
<img alt="lattices" src="site/public/og.png" />
|
|
3
3
|
</picture>
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# lattices
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
macOS workspace manager. Menu bar app, CLI, and a WebSocket API
|
|
8
|
+
so your AI agents can control the desktop.
|
|
8
9
|
|
|
9
|
-
|
|
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
|
|
17
|
+
npm install -g @arach/lattices
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
## Quick start
|
|
18
21
|
|
|
19
22
|
```sh
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 `.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
Configure in `~/.lattice/workspace.json`:
|
|
99
|
+
Configure in `~/.lattices/workspace.json`:
|
|
107
100
|
|
|
108
101
|
```json
|
|
109
102
|
{
|
|
110
|
-
"
|
|
111
|
-
"groups": [
|
|
103
|
+
"layers": [
|
|
112
104
|
{
|
|
113
|
-
"id": "
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
{ "path": "/Users/you/dev/
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
136
|
+
## CLI
|
|
136
137
|
|
|
137
138
|
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
-
|
|
152
|
-
-
|
|
153
|
-
|
|
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
|
+
}
|
package/app/Sources/App.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
-
///
|
|
4
|
-
///
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
}
|