@lattices/cli 0.4.1 → 0.4.5
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 +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/ActionRow.swift +43 -26
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +91 -30
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +53 -16
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +398 -186
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/WorkspaceManager.swift +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +65 -1
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -58,6 +58,9 @@ To build a signed, notarized DMG for distribution:
|
|
|
58
58
|
```sh
|
|
59
59
|
# Requires a Developer ID certificate and notarytool keychain profile
|
|
60
60
|
./scripts/build-dmg.sh
|
|
61
|
+
|
|
62
|
+
# Update v<package.json version> and upload the DMG to GitHub Releases
|
|
63
|
+
./scripts/ship.sh
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
## Quick start
|
package/app/Info.plist
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
17
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
18
|
+
<string>0.4.5</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.5</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
17
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
18
|
+
<string>0.4.5</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.5</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
Binary file
|
package/app/Package.swift
CHANGED
|
@@ -4,9 +4,15 @@ import PackageDescription
|
|
|
4
4
|
let package = Package(
|
|
5
5
|
name: "Lattices",
|
|
6
6
|
platforms: [.macOS(.v13)],
|
|
7
|
+
dependencies: [
|
|
8
|
+
.package(path: "../swift")
|
|
9
|
+
],
|
|
7
10
|
targets: [
|
|
8
11
|
.executableTarget(
|
|
9
12
|
name: "Lattices",
|
|
13
|
+
dependencies: [
|
|
14
|
+
.product(name: "DeckKit", package: "swift")
|
|
15
|
+
],
|
|
10
16
|
path: "Sources",
|
|
11
17
|
resources: [
|
|
12
18
|
.copy("../Resources/tap.wav"),
|
|
@@ -2,9 +2,9 @@ import SwiftUI
|
|
|
2
2
|
|
|
3
3
|
/// A single action row with shortcut badge, label, optional icon, and hotkey hint.
|
|
4
4
|
struct ActionRow: View {
|
|
5
|
-
let shortcut: String
|
|
6
5
|
let label: String
|
|
7
|
-
var
|
|
6
|
+
var detail: String? = nil
|
|
7
|
+
var hotkeyTokens: [String] = []
|
|
8
8
|
var icon: String? = nil
|
|
9
9
|
var accentColor: Color = Palette.textDim
|
|
10
10
|
var action: () -> Void
|
|
@@ -14,41 +14,58 @@ struct ActionRow: View {
|
|
|
14
14
|
var body: some View {
|
|
15
15
|
Button(action: action) {
|
|
16
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
17
|
// Icon
|
|
28
18
|
if let icon {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
ZStack {
|
|
20
|
+
RoundedRectangle(cornerRadius: 5)
|
|
21
|
+
.fill(accentColor.opacity(isHovered ? 0.18 : 0.12))
|
|
22
|
+
Image(systemName: icon)
|
|
23
|
+
.font(.system(size: 11, weight: .medium))
|
|
24
|
+
.foregroundColor(isHovered ? Palette.text : accentColor)
|
|
25
|
+
}
|
|
26
|
+
.frame(width: 22, height: 22)
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
// Label
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
VStack(alignment: .leading, spacing: detail == nil ? 0 : 2) {
|
|
31
|
+
Text(label)
|
|
32
|
+
.font(Typo.body(12))
|
|
33
|
+
.foregroundColor(isHovered ? Palette.text : Palette.textDim)
|
|
34
|
+
.lineLimit(1)
|
|
35
|
+
|
|
36
|
+
if let detail {
|
|
37
|
+
Text(detail)
|
|
38
|
+
.font(Typo.mono(9))
|
|
39
|
+
.foregroundColor(Palette.textMuted)
|
|
40
|
+
.lineLimit(1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
40
43
|
|
|
41
44
|
Spacer()
|
|
42
45
|
|
|
43
46
|
// Hotkey
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if !hotkeyTokens.isEmpty {
|
|
48
|
+
HStack(spacing: 4) {
|
|
49
|
+
ForEach(hotkeyTokens, id: \.self) { token in
|
|
50
|
+
Text(token)
|
|
51
|
+
.font(Typo.monoBold(8))
|
|
52
|
+
.foregroundColor(Palette.textMuted)
|
|
53
|
+
.padding(.horizontal, token.count > 3 ? 6 : 5)
|
|
54
|
+
.padding(.vertical, 3)
|
|
55
|
+
.background(
|
|
56
|
+
RoundedRectangle(cornerRadius: 4)
|
|
57
|
+
.fill(Palette.surface)
|
|
58
|
+
.overlay(
|
|
59
|
+
RoundedRectangle(cornerRadius: 4)
|
|
60
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
48
65
|
}
|
|
49
66
|
}
|
|
50
|
-
.padding(.horizontal,
|
|
51
|
-
.padding(.vertical,
|
|
67
|
+
.padding(.horizontal, 12)
|
|
68
|
+
.padding(.vertical, 8)
|
|
52
69
|
.background(
|
|
53
70
|
RoundedRectangle(cornerRadius: 5)
|
|
54
71
|
.fill(isHovered ? Palette.surfaceHov : Color.clear)
|
package/app/Sources/App.swift
CHANGED
|
@@ -6,5 +6,15 @@ struct LatticesApp: App {
|
|
|
6
6
|
|
|
7
7
|
var body: some Scene {
|
|
8
8
|
Settings { EmptyView() }
|
|
9
|
+
.commands {
|
|
10
|
+
CommandGroup(after: .appInfo) {
|
|
11
|
+
Button("Update Lattices…") {
|
|
12
|
+
AppUpdater.shared.promptForUpdate()
|
|
13
|
+
}
|
|
14
|
+
.disabled(!AppUpdater.shared.canUpdate)
|
|
15
|
+
|
|
16
|
+
Divider()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
}
|
|
10
20
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
|
+
extension Notification.Name {
|
|
5
|
+
static let latticesPopoverWillShow = Notification.Name("latticesPopoverWillShow")
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
/// Manages the NSStatusItem (menu bar icon), left-click popover, and right-click context menu.
|
|
5
9
|
/// Replaces the previous SwiftUI MenuBarExtra approach for full click-event control.
|
|
6
10
|
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
11
|
+
private static weak var shared: AppDelegate?
|
|
7
12
|
|
|
8
13
|
private var statusItem: NSStatusItem!
|
|
9
14
|
private var popover: NSPopover?
|
|
@@ -45,6 +50,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
45
50
|
/// based on whether any managed windows are open.
|
|
46
51
|
static func updateActivationPolicy() {
|
|
47
52
|
let hasVisibleWindow =
|
|
53
|
+
(Self.shared?.popover?.isShown == true) ||
|
|
48
54
|
CommandModeWindow.shared.isVisible ||
|
|
49
55
|
CommandPaletteWindow.shared.isVisible ||
|
|
50
56
|
MainWindow.shared.isVisible ||
|
|
@@ -60,6 +66,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
69
|
+
Self.shared = self
|
|
63
70
|
NSApp.setActivationPolicy(.accessory)
|
|
64
71
|
NSApp.appearance = NSAppearance(named: .darkAqua)
|
|
65
72
|
|
|
@@ -82,8 +89,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
82
89
|
let store = HotkeyStore.shared
|
|
83
90
|
store.register(action: .palette) { CommandPaletteWindow.shared.toggle() }
|
|
84
91
|
store.register(action: .unifiedWindow) { ScreenMapWindowController.shared.toggle() }
|
|
85
|
-
store.register(action: .bezel) {
|
|
86
|
-
store.register(action: .cheatSheet) {
|
|
92
|
+
store.register(action: .bezel) { Self.showWorkspaceInspector() }
|
|
93
|
+
store.register(action: .cheatSheet) { SettingsWindowController.shared.show() }
|
|
87
94
|
store.register(action: .voiceCommand) {
|
|
88
95
|
DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
|
|
89
96
|
VoiceCommandWindow.shared.toggle()
|
|
@@ -103,7 +110,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
103
110
|
|
|
104
111
|
// Pre-render HUD panels off-screen for instant first open
|
|
105
112
|
DispatchQueue.main.async { HUDController.shared.warmUp() }
|
|
113
|
+
// Pre-build the menu bar popover so the first click doesn't pay the SwiftUI mount cost.
|
|
114
|
+
// Touching `.view` forces NSHostingController to materialize the SwiftUI view tree.
|
|
115
|
+
DispatchQueue.main.async { [weak self] in
|
|
116
|
+
guard let self = self else { return }
|
|
117
|
+
let p = self.makePopover()
|
|
118
|
+
_ = p.contentViewController?.view
|
|
119
|
+
}
|
|
106
120
|
store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
|
|
121
|
+
WindowDragSnapController.shared.start()
|
|
122
|
+
MouseGestureController.shared.start()
|
|
107
123
|
|
|
108
124
|
// Session layer cycling
|
|
109
125
|
store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
|
|
@@ -140,7 +156,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
140
156
|
for (action, position) in tileMap {
|
|
141
157
|
store.register(action: action) { WindowTiler.tileFrontmostViaAX(to: position) }
|
|
142
158
|
}
|
|
143
|
-
store.register(action: .tileDistribute) { WindowTiler.distributeVisible() }
|
|
159
|
+
store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
|
|
160
|
+
store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
|
|
144
161
|
|
|
145
162
|
// Onboarding on first launch; otherwise just check permissions
|
|
146
163
|
if !OnboardingWindowController.shared.showIfNeeded() {
|
|
@@ -157,8 +174,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
157
174
|
ProcessModel.shared.start()
|
|
158
175
|
LatticesApi.setup()
|
|
159
176
|
DaemonServer.shared.start()
|
|
177
|
+
LatticesCompanionBridgeServer.shared.start()
|
|
160
178
|
AgentPool.shared.start()
|
|
161
|
-
HandsOffSession.shared.start()
|
|
162
179
|
diag.finish(tBoot)
|
|
163
180
|
|
|
164
181
|
// --diagnostics flag: auto-open diagnostics panel on launch
|
|
@@ -168,14 +185,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
187
|
|
|
171
|
-
// --screen-map flag: auto-open
|
|
188
|
+
// --screen-map flag: auto-open layout on launch
|
|
172
189
|
if CommandLine.arguments.contains("--screen-map") {
|
|
173
190
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
174
|
-
ScreenMapWindowController.shared.
|
|
191
|
+
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
175
192
|
}
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
|
|
196
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
197
|
+
LatticesCompanionBridgeServer.shared.stop()
|
|
198
|
+
DaemonServer.shared.stop()
|
|
199
|
+
}
|
|
200
|
+
|
|
179
201
|
// MARK: - Status item click handler
|
|
180
202
|
|
|
181
203
|
@objc private func statusItemClicked(_ sender: Any?) {
|
|
@@ -185,13 +207,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
185
207
|
// Right-click → context menu
|
|
186
208
|
contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
|
|
187
209
|
} else {
|
|
188
|
-
// Left-click → toggle popover
|
|
210
|
+
// Left-click → toggle the menu bar projects popover.
|
|
189
211
|
if let shown = popover, shown.isShown {
|
|
190
212
|
shown.performClose(sender)
|
|
191
213
|
} else {
|
|
192
|
-
|
|
193
|
-
p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
194
|
-
p.contentViewController?.view.window?.makeKey()
|
|
214
|
+
showProjectsPopover()
|
|
195
215
|
}
|
|
196
216
|
}
|
|
197
217
|
}
|
|
@@ -201,14 +221,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
201
221
|
popover?.performClose(nil)
|
|
202
222
|
}
|
|
203
223
|
|
|
204
|
-
///
|
|
205
|
-
///
|
|
224
|
+
/// Cached popover — built lazily on first click, reused on every subsequent open.
|
|
225
|
+
/// Keeping the SwiftUI view tree alive avoids rebuilding on each click (slow first paint).
|
|
226
|
+
/// Data refresh is driven from `popoverWillShow` + a notification MainView listens to.
|
|
206
227
|
private func makePopover() -> NSPopover {
|
|
228
|
+
if let p = popover { return p }
|
|
207
229
|
let t = DiagnosticLog.shared.startTimed("makePopover")
|
|
208
230
|
let p = NSPopover()
|
|
209
231
|
p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
|
|
210
232
|
p.behavior = .transient
|
|
211
|
-
p.contentSize = NSSize(width: 380, height:
|
|
233
|
+
p.contentSize = NSSize(width: 380, height: 300)
|
|
212
234
|
p.appearance = NSAppearance(named: .darkAqua)
|
|
213
235
|
p.delegate = self
|
|
214
236
|
popover = p
|
|
@@ -216,10 +238,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
216
238
|
return p
|
|
217
239
|
}
|
|
218
240
|
|
|
241
|
+
private func showProjectsPopover() {
|
|
242
|
+
guard let button = statusItem.button else { return }
|
|
243
|
+
let p = makePopover()
|
|
244
|
+
p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
245
|
+
p.contentViewController?.view.window?.makeKey()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func popoverWillShow(_ notification: Notification) {
|
|
249
|
+
Self.updateActivationPolicy()
|
|
250
|
+
NotificationCenter.default.post(name: .latticesPopoverWillShow, object: nil)
|
|
251
|
+
}
|
|
252
|
+
|
|
219
253
|
func popoverDidClose(_ notification: Notification) {
|
|
220
|
-
|
|
221
|
-
popover?.contentViewController = nil
|
|
222
|
-
popover = nil
|
|
254
|
+
Self.updateActivationPolicy()
|
|
223
255
|
}
|
|
224
256
|
|
|
225
257
|
// MARK: - Context menu
|
|
@@ -228,12 +260,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
228
260
|
let menu = NSMenu()
|
|
229
261
|
|
|
230
262
|
let actions: [(String, String, Selector)] = [
|
|
263
|
+
("Home", "", #selector(menuWorkspace)),
|
|
264
|
+
("Layout", "", #selector(menuLayout)),
|
|
265
|
+
("Search", "", #selector(menuSearch)),
|
|
231
266
|
("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
|
|
232
|
-
("Unified Window", "", #selector(menuScreenMap)),
|
|
233
|
-
("HUD", "", #selector(menuHUD)),
|
|
234
|
-
("Window Bezel", "", #selector(menuWindowBezel)),
|
|
235
|
-
("Cheat Sheet", "", #selector(menuCheatSheet)),
|
|
236
|
-
("Omni Search", "", #selector(menuOmniSearch)),
|
|
237
267
|
]
|
|
238
268
|
for (title, shortcut, action) in actions {
|
|
239
269
|
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
@@ -246,14 +276,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
246
276
|
|
|
247
277
|
menu.addItem(.separator())
|
|
248
278
|
|
|
249
|
-
let
|
|
279
|
+
let cliActions: [(String, Selector)] = [
|
|
280
|
+
("Projects…", #selector(menuProjects)),
|
|
281
|
+
("Initialize Project in Terminal…", #selector(menuInitializeProject)),
|
|
282
|
+
("Launch Project in Terminal…", #selector(menuLaunchProject)),
|
|
283
|
+
]
|
|
284
|
+
for (title, action) in cliActions {
|
|
285
|
+
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
286
|
+
item.target = self
|
|
287
|
+
menu.addItem(item)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
menu.addItem(.separator())
|
|
291
|
+
|
|
292
|
+
let update = NSMenuItem(title: "Update Lattices…", action: #selector(menuUpdate), keyEquivalent: "")
|
|
293
|
+
update.target = self
|
|
294
|
+
menu.addItem(update)
|
|
295
|
+
|
|
296
|
+
menu.addItem(.separator())
|
|
297
|
+
|
|
298
|
+
let settings = NSMenuItem(title: "Help & Settings…", action: #selector(menuSettings), keyEquivalent: ",")
|
|
250
299
|
settings.target = self
|
|
251
300
|
menu.addItem(settings)
|
|
252
301
|
|
|
253
|
-
let diag = NSMenuItem(title: "Diagnostics", action: #selector(menuDiagnostics), keyEquivalent: "")
|
|
254
|
-
diag.target = self
|
|
255
|
-
menu.addItem(diag)
|
|
256
|
-
|
|
257
302
|
menu.addItem(.separator())
|
|
258
303
|
|
|
259
304
|
let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
|
|
@@ -264,12 +309,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
264
309
|
}
|
|
265
310
|
|
|
266
311
|
@objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
|
|
267
|
-
@objc private func
|
|
312
|
+
@objc private func menuWorkspace() { ScreenMapWindowController.shared.showPage(.home) }
|
|
313
|
+
@objc private func menuLayout() { ScreenMapWindowController.shared.showPage(.screenMap) }
|
|
314
|
+
@objc private func menuSearch() { ScreenMapWindowController.shared.showPage(.desktopInventory) }
|
|
315
|
+
@objc private func menuDocs() { SettingsWindowController.shared.show() }
|
|
316
|
+
@objc private func menuProjects() { DispatchQueue.main.async { self.showProjectsPopover() } }
|
|
317
|
+
@objc private func menuInitializeProject() { CliActionLauncher.initializeProjectInTerminal() }
|
|
318
|
+
@objc private func menuLaunchProject() { CliActionLauncher.launchProjectInTerminal() }
|
|
268
319
|
@objc private func menuHUD() { HUDController.shared.toggle() }
|
|
269
|
-
@objc private func menuWindowBezel() {
|
|
270
|
-
@objc private func menuCheatSheet() {
|
|
320
|
+
@objc private func menuWindowBezel() { Self.showWorkspaceInspector() }
|
|
321
|
+
@objc private func menuCheatSheet() { SettingsWindowController.shared.show() }
|
|
271
322
|
@objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
|
|
323
|
+
@MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
|
|
272
324
|
@objc private func menuSettings() { SettingsWindowController.shared.show() }
|
|
273
|
-
@objc private func menuDiagnostics() { DiagnosticWindow.shared.toggle() }
|
|
274
325
|
@objc private func menuQuit() { NSApp.terminate(nil) }
|
|
326
|
+
|
|
327
|
+
private static func showWorkspaceInspector() {
|
|
328
|
+
guard let entry = DesktopModel.shared.frontmostWindow(),
|
|
329
|
+
entry.app != "Lattices" else {
|
|
330
|
+
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
ScreenMapWindowController.shared.showWindow(wid: entry.wid)
|
|
335
|
+
}
|
|
275
336
|
}
|
|
@@ -66,6 +66,7 @@ struct AppShellView: View {
|
|
|
66
66
|
ForEach(AppPage.primaryTabs, id: \.rawValue) { tab in
|
|
67
67
|
tabButton(tab)
|
|
68
68
|
}
|
|
69
|
+
|
|
69
70
|
Spacer()
|
|
70
71
|
}
|
|
71
72
|
.padding(.horizontal, 12)
|
|
@@ -90,6 +91,7 @@ struct AppShellView: View {
|
|
|
90
91
|
.foregroundColor(isActive ? Palette.text : Palette.textMuted)
|
|
91
92
|
.padding(.horizontal, 12)
|
|
92
93
|
.padding(.vertical, 6)
|
|
94
|
+
.contentShape(Rectangle())
|
|
93
95
|
.background(
|
|
94
96
|
RoundedRectangle(cornerRadius: 6)
|
|
95
97
|
.fill(isActive ? Palette.surfaceHov : Color.clear)
|
|
@@ -13,6 +13,20 @@ enum AppType: String, CaseIterable {
|
|
|
13
13
|
var label: String { rawValue }
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
enum AppGrouping {
|
|
17
|
+
case type(AppType)
|
|
18
|
+
case exactApp(String)
|
|
19
|
+
|
|
20
|
+
var label: String {
|
|
21
|
+
switch self {
|
|
22
|
+
case .type(let type):
|
|
23
|
+
return type.label
|
|
24
|
+
case .exactApp(let appName):
|
|
25
|
+
return appName
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
enum AppTypeClassifier {
|
|
17
31
|
private static let nameMap: [String: AppType] = [
|
|
18
32
|
// Terminals
|
|
@@ -67,4 +81,26 @@ enum AppTypeClassifier {
|
|
|
67
81
|
if lower.contains("slack") || lower.contains("discord") || lower.contains("chat") || lower.contains("teams") { return .chat }
|
|
68
82
|
return .other
|
|
69
83
|
}
|
|
84
|
+
|
|
85
|
+
static func grouping(for appName: String) -> AppGrouping {
|
|
86
|
+
switch classify(appName) {
|
|
87
|
+
case .system, .other:
|
|
88
|
+
return .exactApp(appName)
|
|
89
|
+
case let type:
|
|
90
|
+
return .type(type)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static func matches(_ appName: String, grouping: AppGrouping) -> Bool {
|
|
95
|
+
switch grouping {
|
|
96
|
+
case .type(let type):
|
|
97
|
+
return classify(appName) == type
|
|
98
|
+
case .exactApp(let exactApp):
|
|
99
|
+
return appName.localizedCaseInsensitiveCompare(exactApp) == .orderedSame
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static func matches(_ appName: String, type: AppType) -> Bool {
|
|
104
|
+
classify(appName) == type
|
|
105
|
+
}
|
|
70
106
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Combine
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
final class AppUpdater: ObservableObject {
|
|
7
|
+
static let shared = AppUpdater()
|
|
8
|
+
|
|
9
|
+
@Published private(set) var isUpdating = false
|
|
10
|
+
@Published private(set) var statusMessage: String?
|
|
11
|
+
|
|
12
|
+
private init() {}
|
|
13
|
+
|
|
14
|
+
var currentVersion: String { LatticesRuntime.appVersion }
|
|
15
|
+
|
|
16
|
+
var canUpdate: Bool {
|
|
17
|
+
LatticesRuntime.bunPath != nil && LatticesRuntime.appHelperScriptPath != nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var unavailableReason: String? {
|
|
21
|
+
if LatticesRuntime.bunPath == nil {
|
|
22
|
+
return "Install Bun to enable in-app updates."
|
|
23
|
+
}
|
|
24
|
+
if LatticesRuntime.appHelperScriptPath == nil {
|
|
25
|
+
return "Launch Lattices via `lattices app` so the updater can find the CLI bundle."
|
|
26
|
+
}
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func promptForUpdate() {
|
|
31
|
+
guard canUpdate else {
|
|
32
|
+
presentAlert(
|
|
33
|
+
title: "Update Unavailable",
|
|
34
|
+
message: unavailableReason ?? "Lattices could not locate its updater."
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let alert = NSAlert()
|
|
40
|
+
alert.alertStyle = .informational
|
|
41
|
+
alert.messageText = "Install the latest Lattices app update?"
|
|
42
|
+
alert.informativeText = "Lattices will download the latest released app bundle, close, and relaunch when the update is ready."
|
|
43
|
+
alert.addButton(withTitle: "Update")
|
|
44
|
+
alert.addButton(withTitle: "Cancel")
|
|
45
|
+
|
|
46
|
+
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
|
47
|
+
startDetachedUpdate()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func startDetachedUpdate() {
|
|
51
|
+
guard !isUpdating else { return }
|
|
52
|
+
guard let bunPath = LatticesRuntime.bunPath,
|
|
53
|
+
let scriptPath = LatticesRuntime.appHelperScriptPath else {
|
|
54
|
+
presentAlert(
|
|
55
|
+
title: "Update Unavailable",
|
|
56
|
+
message: unavailableReason ?? "Lattices could not locate its updater."
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let proc = Process()
|
|
62
|
+
proc.executableURL = URL(fileURLWithPath: bunPath)
|
|
63
|
+
proc.arguments = [scriptPath, "update", "--detach", "--launch"]
|
|
64
|
+
if let cliRoot = LatticesRuntime.cliRoot {
|
|
65
|
+
proc.currentDirectoryURL = URL(fileURLWithPath: cliRoot)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var env = ProcessInfo.processInfo.environment
|
|
69
|
+
env.removeValue(forKey: "CLAUDECODE")
|
|
70
|
+
proc.environment = env
|
|
71
|
+
|
|
72
|
+
do {
|
|
73
|
+
try proc.run()
|
|
74
|
+
isUpdating = true
|
|
75
|
+
statusMessage = "Updating to the latest release. Lattices will relaunch when it's ready."
|
|
76
|
+
} catch {
|
|
77
|
+
presentAlert(
|
|
78
|
+
title: "Update Failed",
|
|
79
|
+
message: "Lattices could not start the updater.\n\n\(error.localizedDescription)"
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func presentAlert(title: String, message: String) {
|
|
85
|
+
let alert = NSAlert()
|
|
86
|
+
alert.alertStyle = .warning
|
|
87
|
+
alert.messageText = title
|
|
88
|
+
alert.informativeText = message
|
|
89
|
+
alert.addButton(withTitle: "OK")
|
|
90
|
+
alert.runModal()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -254,6 +254,7 @@ struct CheatSheetView: View {
|
|
|
254
254
|
// Center + Distribute
|
|
255
255
|
shortcutRow(action: .tileCenter)
|
|
256
256
|
shortcutRow(action: .tileDistribute)
|
|
257
|
+
shortcutRow(action: .tileTypeGrid)
|
|
257
258
|
|
|
258
259
|
// Hovered shortcut detail
|
|
259
260
|
if let hovered = hoveredAction, let binding = hotkeyStore.bindings[hovered] {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
enum CliActionLauncher {
|
|
4
|
+
private static var defaultDirectory: String {
|
|
5
|
+
let root = Preferences.shared.scanRoot
|
|
6
|
+
return root.isEmpty ? NSHomeDirectory() : root
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
private static func chooseProjectDirectory(message: String, prompt: String) -> String? {
|
|
10
|
+
let panel = NSOpenPanel()
|
|
11
|
+
panel.message = message
|
|
12
|
+
panel.prompt = prompt
|
|
13
|
+
panel.canChooseFiles = false
|
|
14
|
+
panel.canChooseDirectories = true
|
|
15
|
+
panel.allowsMultipleSelection = false
|
|
16
|
+
panel.directoryURL = URL(fileURLWithPath: defaultDirectory)
|
|
17
|
+
return panel.runModal() == .OK ? panel.url?.path : nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static func initializeProjectInTerminal() {
|
|
21
|
+
guard let directory = chooseProjectDirectory(
|
|
22
|
+
message: "Choose a project folder to initialize with Lattices.",
|
|
23
|
+
prompt: "Initialize"
|
|
24
|
+
) else { return }
|
|
25
|
+
|
|
26
|
+
Preferences.shared.terminal.launch(
|
|
27
|
+
command: "lattices init && lattices",
|
|
28
|
+
in: directory
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static func launchProjectInTerminal() {
|
|
33
|
+
guard let directory = chooseProjectDirectory(
|
|
34
|
+
message: "Choose a project folder to launch with Lattices.",
|
|
35
|
+
prompt: "Launch"
|
|
36
|
+
) else { return }
|
|
37
|
+
|
|
38
|
+
Preferences.shared.terminal.launch(
|
|
39
|
+
command: "lattices",
|
|
40
|
+
in: directory
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static func installTmuxInTerminal() {
|
|
45
|
+
Preferences.shared.terminal.launch(
|
|
46
|
+
command: "brew install tmux",
|
|
47
|
+
in: defaultDirectory
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -121,23 +121,6 @@ struct CommandModeView: View {
|
|
|
121
121
|
|
|
122
122
|
Spacer()
|
|
123
123
|
|
|
124
|
-
if let layer = state.inventory.activeLayer {
|
|
125
|
-
HStack(spacing: 4) {
|
|
126
|
-
Text("Layer: \(layer)")
|
|
127
|
-
.font(Typo.mono(10))
|
|
128
|
-
.foregroundColor(Palette.running)
|
|
129
|
-
|
|
130
|
-
Text("[\(state.inventory.layerCount > 0 ? "\(WorkspaceManager.shared.activeLayerIndex + 1)/\(state.inventory.layerCount)" : "—")]")
|
|
131
|
-
.font(Typo.mono(10))
|
|
132
|
-
.foregroundColor(Palette.textMuted)
|
|
133
|
-
}
|
|
134
|
-
.padding(.horizontal, 6)
|
|
135
|
-
.padding(.vertical, 2)
|
|
136
|
-
.background(
|
|
137
|
-
RoundedRectangle(cornerRadius: 3)
|
|
138
|
-
.fill(Palette.running.opacity(0.10))
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
124
|
}
|
|
142
125
|
.padding(.horizontal, 16)
|
|
143
126
|
.padding(.vertical, 10)
|
|
@@ -155,15 +138,12 @@ struct CommandModeView: View {
|
|
|
155
138
|
private var inventoryGrid: some View {
|
|
156
139
|
ScrollView {
|
|
157
140
|
LazyVStack(alignment: .leading, spacing: 0) {
|
|
158
|
-
let
|
|
159
|
-
if
|
|
141
|
+
let items = state.inventory.items
|
|
142
|
+
if items.isEmpty {
|
|
160
143
|
emptyState
|
|
161
144
|
} else {
|
|
162
|
-
ForEach(
|
|
163
|
-
|
|
164
|
-
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
165
|
-
inventoryRow(item)
|
|
166
|
-
}
|
|
145
|
+
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
146
|
+
inventoryRow(item)
|
|
167
147
|
}
|
|
168
148
|
}
|
|
169
149
|
}
|