@lattices/cli 0.4.2 → 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/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +90 -34
- 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 +15 -4
- 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 +351 -191
- 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 +62 -0
- 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 +2 -1
|
@@ -1,15 +1,67 @@
|
|
|
1
|
+
import DeckKit
|
|
1
2
|
import SwiftUI
|
|
2
3
|
|
|
3
4
|
/// Settings content with internal General / Shortcuts tabs.
|
|
4
5
|
/// Can also render the Docs page when `page == .docs`.
|
|
5
6
|
struct SettingsContentView: View {
|
|
7
|
+
private enum SettingsSection: String, CaseIterable, Identifiable {
|
|
8
|
+
case general
|
|
9
|
+
case ai
|
|
10
|
+
case search
|
|
11
|
+
case shortcuts
|
|
12
|
+
|
|
13
|
+
var id: String { rawValue }
|
|
14
|
+
|
|
15
|
+
var title: String {
|
|
16
|
+
switch self {
|
|
17
|
+
case .general: return "General"
|
|
18
|
+
case .ai: return "AI"
|
|
19
|
+
case .search: return "Search & OCR"
|
|
20
|
+
case .shortcuts: return "Shortcuts"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var icon: String {
|
|
25
|
+
switch self {
|
|
26
|
+
case .general: return "slider.horizontal.3"
|
|
27
|
+
case .ai: return "sparkles"
|
|
28
|
+
case .search: return "text.viewfinder"
|
|
29
|
+
case .shortcuts: return "command"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var eyebrow: String {
|
|
34
|
+
switch self {
|
|
35
|
+
case .general: return "Workspace"
|
|
36
|
+
case .ai: return "Agents"
|
|
37
|
+
case .search: return "Indexing"
|
|
38
|
+
case .shortcuts: return "Controls"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var summary: String {
|
|
43
|
+
switch self {
|
|
44
|
+
case .general:
|
|
45
|
+
return "Terminal defaults, scan roots, window snapping, and app updates."
|
|
46
|
+
case .ai:
|
|
47
|
+
return "Claude CLI detection plus advisor model and spending controls."
|
|
48
|
+
case .search:
|
|
49
|
+
return "OCR cadence, quality, and recent capture visibility."
|
|
50
|
+
case .shortcuts:
|
|
51
|
+
return "A full map of global hotkeys for workspace movement and tmux flow."
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
6
56
|
var page: AppPage = .settings
|
|
7
57
|
@ObservedObject var prefs: Preferences
|
|
8
58
|
@ObservedObject var scanner: ProjectScanner
|
|
9
59
|
@ObservedObject var hotkeyStore: HotkeyStore = .shared
|
|
60
|
+
@ObservedObject var appUpdater: AppUpdater = .shared
|
|
61
|
+
@ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
|
|
10
62
|
var onBack: (() -> Void)? = nil
|
|
11
63
|
|
|
12
|
-
@State private var selectedTab =
|
|
64
|
+
@State private var selectedTab: SettingsSection = .general
|
|
13
65
|
|
|
14
66
|
var body: some View {
|
|
15
67
|
VStack(spacing: 0) {
|
|
@@ -30,12 +82,7 @@ struct SettingsContentView: View {
|
|
|
30
82
|
// MARK: - Back Bar
|
|
31
83
|
|
|
32
84
|
private var currentTabLabel: String {
|
|
33
|
-
|
|
34
|
-
case "general": return "General"
|
|
35
|
-
case "search": return "Search & OCR"
|
|
36
|
-
case "shortcuts": return "Shortcuts"
|
|
37
|
-
default: return page.label
|
|
38
|
-
}
|
|
85
|
+
page == .docs ? "Docs" : selectedTab.title
|
|
39
86
|
}
|
|
40
87
|
|
|
41
88
|
private var backBar: some View {
|
|
@@ -64,64 +111,136 @@ struct SettingsContentView: View {
|
|
|
64
111
|
}
|
|
65
112
|
}
|
|
66
113
|
|
|
67
|
-
// MARK: - Settings Body
|
|
114
|
+
// MARK: - Settings Body
|
|
68
115
|
|
|
69
116
|
private var settingsBody: some View {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
HStack(spacing: 0) {
|
|
118
|
+
settingsSidebar
|
|
119
|
+
.frame(width: 220, alignment: .top)
|
|
120
|
+
|
|
121
|
+
Rectangle()
|
|
122
|
+
.fill(Palette.border)
|
|
123
|
+
.frame(width: 0.5)
|
|
124
|
+
.frame(maxHeight: .infinity)
|
|
125
|
+
|
|
126
|
+
VStack(spacing: 0) {
|
|
127
|
+
settingsSectionHero(selectedTab)
|
|
128
|
+
|
|
129
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
130
|
+
|
|
131
|
+
selectedSectionContent
|
|
78
132
|
}
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
}
|
|
134
|
+
}
|
|
81
135
|
|
|
82
|
-
|
|
136
|
+
private var settingsSidebar: some View {
|
|
137
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
138
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
139
|
+
Text("SETTINGS")
|
|
140
|
+
.font(Typo.pixel(14))
|
|
141
|
+
.foregroundColor(Palette.textDim)
|
|
142
|
+
.tracking(1)
|
|
143
|
+
Text("Tune how Lattices launches workspaces, listens for commands, and navigates the desktop.")
|
|
144
|
+
.font(Typo.caption(11))
|
|
145
|
+
.foregroundColor(Palette.textMuted)
|
|
146
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
147
|
+
}
|
|
83
148
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
case "ai": aiContent
|
|
89
|
-
default: generalContent
|
|
149
|
+
VStack(spacing: 6) {
|
|
150
|
+
ForEach(SettingsSection.allCases) { section in
|
|
151
|
+
settingsTab(section)
|
|
152
|
+
}
|
|
90
153
|
}
|
|
154
|
+
|
|
155
|
+
Spacer(minLength: 0)
|
|
91
156
|
}
|
|
157
|
+
.padding(16)
|
|
92
158
|
}
|
|
93
159
|
|
|
94
|
-
private func settingsTab(
|
|
95
|
-
let active = selectedTab ==
|
|
160
|
+
private func settingsTab(_ section: SettingsSection) -> some View {
|
|
161
|
+
let active = selectedTab == section
|
|
96
162
|
return Button {
|
|
97
|
-
selectedTab =
|
|
163
|
+
selectedTab = section
|
|
98
164
|
} label: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
165
|
+
HStack(alignment: .top, spacing: 10) {
|
|
166
|
+
Image(systemName: section.icon)
|
|
167
|
+
.font(.system(size: 11, weight: .semibold))
|
|
168
|
+
.foregroundColor(active ? Palette.text : Palette.textMuted)
|
|
169
|
+
.frame(width: 16, alignment: .center)
|
|
170
|
+
|
|
171
|
+
VStack(alignment: .leading, spacing: 3) {
|
|
172
|
+
Text(section.title)
|
|
173
|
+
.font(Typo.mono(11))
|
|
174
|
+
.foregroundColor(active ? Palette.text : Palette.textMuted)
|
|
175
|
+
|
|
176
|
+
Text(section.summary)
|
|
177
|
+
.font(Typo.caption(9.5))
|
|
178
|
+
.foregroundColor(Palette.textMuted.opacity(active ? 0.9 : 0.7))
|
|
179
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Spacer(minLength: 0)
|
|
183
|
+
}
|
|
184
|
+
.padding(.horizontal, 10)
|
|
185
|
+
.padding(.vertical, 9)
|
|
186
|
+
.contentShape(RoundedRectangle(cornerRadius: 8))
|
|
187
|
+
.background(
|
|
188
|
+
ZStack {
|
|
189
|
+
if active {
|
|
190
|
+
RoundedRectangle(cornerRadius: 8)
|
|
191
|
+
.fill(Color.white.opacity(0.06))
|
|
192
|
+
RoundedRectangle(cornerRadius: 8)
|
|
193
|
+
.strokeBorder(
|
|
194
|
+
LinearGradient(
|
|
195
|
+
colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
|
|
196
|
+
startPoint: .top,
|
|
197
|
+
endPoint: .bottom
|
|
198
|
+
),
|
|
199
|
+
lineWidth: 0.5
|
|
200
|
+
)
|
|
119
201
|
}
|
|
120
|
-
|
|
202
|
+
}
|
|
203
|
+
)
|
|
121
204
|
}
|
|
122
205
|
.buttonStyle(.plain)
|
|
123
206
|
}
|
|
124
207
|
|
|
208
|
+
private func settingsSectionHero(_ section: SettingsSection) -> some View {
|
|
209
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
210
|
+
Text(section.eyebrow.uppercased())
|
|
211
|
+
.font(Typo.pixel(14))
|
|
212
|
+
.foregroundColor(Palette.textDim)
|
|
213
|
+
.tracking(1)
|
|
214
|
+
|
|
215
|
+
Text(section.title)
|
|
216
|
+
.font(Typo.heading(16))
|
|
217
|
+
.foregroundColor(Palette.text)
|
|
218
|
+
|
|
219
|
+
Text(section.summary)
|
|
220
|
+
.font(Typo.caption(11))
|
|
221
|
+
.foregroundColor(Palette.textMuted)
|
|
222
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
223
|
+
}
|
|
224
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
225
|
+
.padding(.horizontal, 20)
|
|
226
|
+
.padding(.vertical, 14)
|
|
227
|
+
.background(Palette.bg)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@ViewBuilder
|
|
231
|
+
private var selectedSectionContent: some View {
|
|
232
|
+
switch selectedTab {
|
|
233
|
+
case .general:
|
|
234
|
+
generalContent
|
|
235
|
+
case .ai:
|
|
236
|
+
aiContent
|
|
237
|
+
case .search:
|
|
238
|
+
searchOcrContent
|
|
239
|
+
case .shortcuts:
|
|
240
|
+
shortcutsContent
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
125
244
|
// MARK: - Sticky section header
|
|
126
245
|
|
|
127
246
|
private func stickyHeader(_ title: String) -> some View {
|
|
@@ -148,6 +267,62 @@ struct SettingsContentView: View {
|
|
|
148
267
|
private var generalContent: some View {
|
|
149
268
|
ScrollView {
|
|
150
269
|
VStack(alignment: .leading, spacing: 12) {
|
|
270
|
+
settingsCard {
|
|
271
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
272
|
+
HStack(alignment: .center, spacing: 8) {
|
|
273
|
+
Image(systemName: "arrow.down.circle")
|
|
274
|
+
.font(.system(size: 11, weight: .medium))
|
|
275
|
+
.foregroundColor(Palette.running)
|
|
276
|
+
Text("Lattices app")
|
|
277
|
+
.font(Typo.mono(12))
|
|
278
|
+
.foregroundColor(Palette.text)
|
|
279
|
+
Spacer()
|
|
280
|
+
Text("v\(appUpdater.currentVersion)")
|
|
281
|
+
.font(Typo.caption(10))
|
|
282
|
+
.foregroundColor(Palette.textMuted)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Text("Install the latest published app build without leaving the menu bar. The app relaunches when the update finishes.")
|
|
286
|
+
.font(Typo.caption(10))
|
|
287
|
+
.foregroundColor(Palette.textMuted)
|
|
288
|
+
|
|
289
|
+
if let status = appUpdater.statusMessage {
|
|
290
|
+
Text(status)
|
|
291
|
+
.font(Typo.caption(9))
|
|
292
|
+
.foregroundColor(Palette.running.opacity(0.85))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if let reason = appUpdater.unavailableReason {
|
|
296
|
+
Text(reason)
|
|
297
|
+
.font(Typo.caption(9))
|
|
298
|
+
.foregroundColor(Palette.detach.opacity(0.9))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
HStack(spacing: 10) {
|
|
302
|
+
Button {
|
|
303
|
+
appUpdater.promptForUpdate()
|
|
304
|
+
} label: {
|
|
305
|
+
Text(appUpdater.isUpdating ? "Updating…" : "Update Lattices")
|
|
306
|
+
.font(Typo.monoBold(10))
|
|
307
|
+
.foregroundColor(Palette.text)
|
|
308
|
+
.padding(.horizontal, 12)
|
|
309
|
+
.padding(.vertical, 5)
|
|
310
|
+
.background(
|
|
311
|
+
RoundedRectangle(cornerRadius: 4)
|
|
312
|
+
.fill(Palette.surfaceHov)
|
|
313
|
+
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
.buttonStyle(.plain)
|
|
317
|
+
.disabled(appUpdater.isUpdating)
|
|
318
|
+
|
|
319
|
+
Text("CLI: `lattices app update`")
|
|
320
|
+
.font(Typo.caption(9))
|
|
321
|
+
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
151
326
|
// ── Terminal ──
|
|
152
327
|
settingsCard {
|
|
153
328
|
VStack(alignment: .leading, spacing: 8) {
|
|
@@ -266,8 +441,136 @@ struct SettingsContentView: View {
|
|
|
266
441
|
}
|
|
267
442
|
}
|
|
268
443
|
}
|
|
444
|
+
|
|
445
|
+
settingsCard {
|
|
446
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
447
|
+
Text("Window drag snap")
|
|
448
|
+
.font(Typo.mono(11))
|
|
449
|
+
.foregroundColor(Palette.text)
|
|
450
|
+
|
|
451
|
+
HStack {
|
|
452
|
+
Text("Drag-to-snap")
|
|
453
|
+
.font(Typo.mono(10))
|
|
454
|
+
.foregroundColor(Palette.textDim)
|
|
455
|
+
Spacer()
|
|
456
|
+
Toggle("", isOn: $prefs.dragSnapEnabled)
|
|
457
|
+
.toggleStyle(.switch)
|
|
458
|
+
.controlSize(.small)
|
|
459
|
+
.labelsHidden()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
Text("Hold the configured snap modifier while dragging to reveal landing targets and a live preview, then release it to go back to a free drag. Default: Command.")
|
|
463
|
+
.font(Typo.caption(9))
|
|
464
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
465
|
+
|
|
466
|
+
cardDivider
|
|
467
|
+
|
|
468
|
+
Text("Agent-editable rules live in ~/.lattices/snap-zones.json. Changes are picked up on the next drag.")
|
|
469
|
+
.font(Typo.caption(9))
|
|
470
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
settingsCard {
|
|
475
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
476
|
+
Text("Mouse gestures")
|
|
477
|
+
.font(Typo.mono(11))
|
|
478
|
+
.foregroundColor(Palette.text)
|
|
479
|
+
|
|
480
|
+
HStack {
|
|
481
|
+
Text("Middle-click gestures")
|
|
482
|
+
.font(Typo.mono(10))
|
|
483
|
+
.foregroundColor(Palette.textDim)
|
|
484
|
+
Spacer()
|
|
485
|
+
Toggle("", isOn: $prefs.mouseGesturesEnabled)
|
|
486
|
+
.toggleStyle(.switch)
|
|
487
|
+
.controlSize(.small)
|
|
488
|
+
.labelsHidden()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
Text("Rules live in ~/.lattices/mouse-shortcuts.json. The current defaults preserve the working setup: middle-click drag left/right switches Spaces and drag down opens the Screen Map overview.")
|
|
492
|
+
.font(Typo.caption(9))
|
|
493
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
494
|
+
|
|
495
|
+
cardDivider
|
|
496
|
+
|
|
497
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
498
|
+
Text("Active drag mappings")
|
|
499
|
+
.font(Typo.mono(10))
|
|
500
|
+
.foregroundColor(Palette.textDim)
|
|
501
|
+
|
|
502
|
+
ForEach(mouseShortcutStore.summaryLines.prefix(4), id: \.self) { line in
|
|
503
|
+
Text(line)
|
|
504
|
+
.font(Typo.caption(9))
|
|
505
|
+
.foregroundColor(Palette.textMuted.opacity(0.78))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if mouseShortcutStore.summaryLines.isEmpty {
|
|
509
|
+
Text("No active mappings")
|
|
510
|
+
.font(Typo.caption(9))
|
|
511
|
+
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
HStack(spacing: 8) {
|
|
516
|
+
Button {
|
|
517
|
+
mouseShortcutStore.openConfiguration()
|
|
518
|
+
} label: {
|
|
519
|
+
Text("Configure...")
|
|
520
|
+
.font(Typo.monoBold(10))
|
|
521
|
+
.foregroundColor(Palette.text)
|
|
522
|
+
.padding(.horizontal, 12)
|
|
523
|
+
.padding(.vertical, 4)
|
|
524
|
+
.background(
|
|
525
|
+
RoundedRectangle(cornerRadius: 4)
|
|
526
|
+
.fill(Palette.surfaceHov)
|
|
527
|
+
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
.buttonStyle(.plain)
|
|
531
|
+
|
|
532
|
+
Button {
|
|
533
|
+
MouseInputEventViewer.shared.show()
|
|
534
|
+
} label: {
|
|
535
|
+
Text("Open Event Viewer")
|
|
536
|
+
.font(Typo.monoBold(10))
|
|
537
|
+
.foregroundColor(Palette.text)
|
|
538
|
+
.padding(.horizontal, 12)
|
|
539
|
+
.padding(.vertical, 4)
|
|
540
|
+
.background(
|
|
541
|
+
RoundedRectangle(cornerRadius: 4)
|
|
542
|
+
.fill(Palette.surfaceHov)
|
|
543
|
+
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
.buttonStyle(.plain)
|
|
547
|
+
|
|
548
|
+
Button {
|
|
549
|
+
mouseShortcutStore.restoreDefaults()
|
|
550
|
+
} label: {
|
|
551
|
+
Text("Restore Defaults")
|
|
552
|
+
.font(Typo.monoBold(10))
|
|
553
|
+
.foregroundColor(Palette.text)
|
|
554
|
+
.padding(.horizontal, 12)
|
|
555
|
+
.padding(.vertical, 4)
|
|
556
|
+
.background(
|
|
557
|
+
RoundedRectangle(cornerRadius: 4)
|
|
558
|
+
.fill(Palette.surfaceHov)
|
|
559
|
+
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
.buttonStyle(.plain)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
Text("Use Event Viewer to discover what your mouse emits on this machine. The config schema already accepts device selectors, but live gesture matching currently falls back to global rules when macOS doesn't expose the source device.")
|
|
566
|
+
.font(Typo.caption(9))
|
|
567
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
568
|
+
}
|
|
569
|
+
}
|
|
269
570
|
}
|
|
270
571
|
.padding(16)
|
|
572
|
+
.frame(maxWidth: 760, alignment: .leading)
|
|
573
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
271
574
|
}
|
|
272
575
|
}
|
|
273
576
|
|
|
@@ -829,32 +1132,48 @@ struct SettingsContentView: View {
|
|
|
829
1132
|
.padding(.vertical, 3)
|
|
830
1133
|
}
|
|
831
1134
|
|
|
832
|
-
// MARK: - Shortcuts
|
|
1135
|
+
// MARK: - Shortcuts
|
|
833
1136
|
|
|
834
1137
|
private var shortcutsContent: some View {
|
|
835
1138
|
VStack(spacing: 0) {
|
|
836
1139
|
GeometryReader { geo in
|
|
837
|
-
let
|
|
838
|
-
let
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
let
|
|
842
|
-
|
|
1140
|
+
let contentWidth = max(geo.size.width - 40, 320)
|
|
1141
|
+
let sectionColumns = [
|
|
1142
|
+
GridItem(.adaptive(minimum: min(320, contentWidth), maximum: 440), spacing: 16, alignment: .top)
|
|
1143
|
+
]
|
|
1144
|
+
let tilingColumns = contentWidth > 860
|
|
1145
|
+
? [
|
|
1146
|
+
GridItem(.flexible(minimum: 280, maximum: 360), spacing: 16, alignment: .top),
|
|
1147
|
+
GridItem(.flexible(minimum: 320, maximum: 640), spacing: 16, alignment: .top)
|
|
1148
|
+
]
|
|
1149
|
+
: [GridItem(.flexible(minimum: 0, maximum: .infinity), spacing: 16, alignment: .top)]
|
|
843
1150
|
|
|
844
1151
|
ScrollView {
|
|
845
1152
|
VStack(alignment: .leading, spacing: 0) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1153
|
+
VStack(alignment: .leading, spacing: 16) {
|
|
1154
|
+
companionCockpitCard
|
|
1155
|
+
|
|
1156
|
+
shortcutsOverviewCard
|
|
1157
|
+
|
|
1158
|
+
LazyVGrid(columns: sectionColumns, alignment: .leading, spacing: 16) {
|
|
1159
|
+
shortcutsAppCard
|
|
1160
|
+
shortcutsLayersCard
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
shortcutSectionCard(
|
|
1164
|
+
title: "Window Tiling",
|
|
1165
|
+
eyebrow: "Desktop Layout",
|
|
1166
|
+
summary: "See the directional map first, then edit the matching global shortcuts below."
|
|
1167
|
+
) {
|
|
1168
|
+
LazyVGrid(columns: tilingColumns, alignment: .leading, spacing: 16) {
|
|
1169
|
+
shortcutsTilingVisualizer
|
|
1170
|
+
shortcutsTilingEditors
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
shortcutsTmuxCard
|
|
856
1175
|
}
|
|
857
|
-
.padding(.horizontal,
|
|
1176
|
+
.padding(.horizontal, 20)
|
|
858
1177
|
.padding(.vertical, 16)
|
|
859
1178
|
}
|
|
860
1179
|
}
|
|
@@ -865,7 +1184,18 @@ struct SettingsContentView: View {
|
|
|
865
1184
|
separator
|
|
866
1185
|
|
|
867
1186
|
HStack {
|
|
1187
|
+
HStack(spacing: 8) {
|
|
1188
|
+
footerActionButton(icon: "book", label: "Docs") {
|
|
1189
|
+
ScreenMapWindowController.shared.showPage(.docs)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
footerActionButton(icon: "stethoscope", label: "Diagnostics") {
|
|
1193
|
+
DiagnosticWindow.shared.show()
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
868
1197
|
Spacer()
|
|
1198
|
+
|
|
869
1199
|
Button {
|
|
870
1200
|
hotkeyStore.resetAll()
|
|
871
1201
|
} label: {
|
|
@@ -890,112 +1220,406 @@ struct SettingsContentView: View {
|
|
|
890
1220
|
}
|
|
891
1221
|
}
|
|
892
1222
|
|
|
893
|
-
// MARK: - Shortcuts:
|
|
1223
|
+
// MARK: - Shortcuts: Overview
|
|
1224
|
+
|
|
1225
|
+
private var companionCockpitCard: some View {
|
|
1226
|
+
let layout = LatticesCompanionCockpitCatalog.normalized(prefs.companionCockpitLayout)
|
|
1227
|
+
let selectedPage = layout.pages.first(where: { $0.id == selectedCompanionCockpitPageID }) ?? layout.pages.first
|
|
1228
|
+
let categories = LatticesCompanionShortcutCategory.allCases
|
|
1229
|
+
let trustedDevices = companionTrustedDevices(revision: companionTrustRevision)
|
|
1230
|
+
|
|
1231
|
+
return shortcutSectionCard(
|
|
1232
|
+
title: "Companion Cockpit",
|
|
1233
|
+
eyebrow: "iPad & iPhone",
|
|
1234
|
+
summary: "Define the Mac-authored command deck here, then let the companion app render it. Trackpad proxy runs through the same bridge."
|
|
1235
|
+
) {
|
|
1236
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
1237
|
+
HStack(alignment: .top, spacing: 12) {
|
|
1238
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
1239
|
+
Text("Trackpad Proxy")
|
|
1240
|
+
.font(Typo.monoBold(11))
|
|
1241
|
+
.foregroundColor(Palette.text)
|
|
1242
|
+
Text("Enable remote pointer control for the iPad trackpad surface. Accessibility permission is still required on the Mac.")
|
|
1243
|
+
.font(Typo.caption(10.5))
|
|
1244
|
+
.foregroundColor(Palette.textMuted)
|
|
1245
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1246
|
+
}
|
|
894
1247
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1248
|
+
Spacer()
|
|
1249
|
+
|
|
1250
|
+
Toggle("", isOn: $prefs.companionTrackpadEnabled)
|
|
1251
|
+
.toggleStyle(.switch)
|
|
1252
|
+
.labelsHidden()
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1256
|
+
HStack {
|
|
1257
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
1258
|
+
Text("Trusted Devices")
|
|
1259
|
+
.font(Typo.monoBold(11))
|
|
1260
|
+
.foregroundColor(Palette.text)
|
|
1261
|
+
Text("New companions must be approved on the Mac before they can send encrypted bridge requests.")
|
|
1262
|
+
.font(Typo.caption(10.5))
|
|
1263
|
+
.foregroundColor(Palette.textMuted)
|
|
1264
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1265
|
+
}
|
|
898
1266
|
|
|
899
|
-
|
|
1267
|
+
Spacer()
|
|
1268
|
+
|
|
1269
|
+
if trustedDevices.isEmpty == false {
|
|
1270
|
+
Button("Forget All") {
|
|
1271
|
+
LatticesCompanionSecurityCoordinator.shared.clearTrustedDevices()
|
|
1272
|
+
companionTrustRevision += 1
|
|
1273
|
+
}
|
|
1274
|
+
.buttonStyle(.plain)
|
|
1275
|
+
.font(Typo.caption(10.5))
|
|
1276
|
+
.foregroundColor(Palette.textDim)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
1281
|
+
if trustedDevices.isEmpty {
|
|
1282
|
+
Text("No paired iPad or iPhone devices yet.")
|
|
1283
|
+
.font(Typo.caption(10.5))
|
|
1284
|
+
.foregroundColor(Palette.textMuted)
|
|
1285
|
+
} else {
|
|
1286
|
+
ForEach(trustedDevices) { device in
|
|
1287
|
+
HStack(alignment: .top, spacing: 10) {
|
|
1288
|
+
Image(systemName: "iphone.gen3")
|
|
1289
|
+
.font(.system(size: 11, weight: .semibold))
|
|
1290
|
+
.foregroundColor(Palette.textDim)
|
|
1291
|
+
.frame(width: 14)
|
|
1292
|
+
|
|
1293
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
1294
|
+
Text(device.name)
|
|
1295
|
+
.font(Typo.caption(11))
|
|
1296
|
+
.foregroundColor(Palette.text)
|
|
1297
|
+
Text("\(device.fingerprint) · Last seen \(relativeTimestamp(device.lastSeenAt))")
|
|
1298
|
+
.font(Typo.caption(10))
|
|
1299
|
+
.foregroundColor(Palette.textMuted)
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
Spacer(minLength: 0)
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
.padding(12)
|
|
1308
|
+
.background(shortcutsInsetPanel)
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if let selectedPage {
|
|
1312
|
+
Picker("Companion page", selection: $selectedCompanionCockpitPageID) {
|
|
1313
|
+
ForEach(layout.pages) { page in
|
|
1314
|
+
Text(page.title).tag(page.id)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
.pickerStyle(.segmented)
|
|
1318
|
+
|
|
1319
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1320
|
+
if let subtitle = selectedPage.subtitle, !subtitle.isEmpty {
|
|
1321
|
+
Text(subtitle)
|
|
1322
|
+
.font(Typo.caption(10.5))
|
|
1323
|
+
.foregroundColor(Palette.textMuted)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
LazyVGrid(
|
|
1327
|
+
columns: Array(
|
|
1328
|
+
repeating: GridItem(.flexible(minimum: 120, maximum: 220), spacing: 8, alignment: .top),
|
|
1329
|
+
count: max(2, selectedPage.columns)
|
|
1330
|
+
),
|
|
1331
|
+
alignment: .leading,
|
|
1332
|
+
spacing: 8
|
|
1333
|
+
) {
|
|
1334
|
+
ForEach(Array(selectedPage.slotIDs.enumerated()), id: \.offset) { index, shortcutID in
|
|
1335
|
+
companionCockpitSlotMenu(
|
|
1336
|
+
pageID: selectedPage.id,
|
|
1337
|
+
index: index,
|
|
1338
|
+
shortcutID: shortcutID,
|
|
1339
|
+
categories: categories
|
|
1340
|
+
)
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
.padding(12)
|
|
1345
|
+
.background(shortcutsInsetPanel)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
HStack(spacing: 10) {
|
|
1349
|
+
Text("Changes appear in the iPad companion on the next snapshot refresh.")
|
|
1350
|
+
.font(Typo.caption(10.5))
|
|
1351
|
+
.foregroundColor(Palette.textMuted)
|
|
1352
|
+
|
|
1353
|
+
Spacer()
|
|
1354
|
+
|
|
1355
|
+
Button("Reset Companion Layout") {
|
|
1356
|
+
prefs.resetCompanionCockpitLayout()
|
|
1357
|
+
}
|
|
1358
|
+
.buttonStyle(.plain)
|
|
1359
|
+
.font(Typo.caption(10.5))
|
|
1360
|
+
.foregroundColor(Palette.textDim)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
private var shortcutsOverviewCard: some View {
|
|
1367
|
+
shortcutSectionCard(
|
|
1368
|
+
title: "Shortcut Map",
|
|
1369
|
+
eyebrow: "Quick Reference",
|
|
1370
|
+
summary: "Global hotkeys are editable here. tmux shortcuts stay as a built-in reference so you can keep your workspace flow in one place."
|
|
1371
|
+
) {
|
|
1372
|
+
LazyVGrid(
|
|
1373
|
+
columns: [GridItem(.adaptive(minimum: 180, maximum: 240), spacing: 10, alignment: .top)],
|
|
1374
|
+
alignment: .leading,
|
|
1375
|
+
spacing: 10
|
|
1376
|
+
) {
|
|
1377
|
+
shortcutFactCard(
|
|
1378
|
+
icon: "command",
|
|
1379
|
+
title: "Global Hotkeys",
|
|
1380
|
+
detail: "Edit palette, search, voice, and workspace actions without leaving settings."
|
|
1381
|
+
)
|
|
1382
|
+
shortcutFactCard(
|
|
1383
|
+
icon: "rectangle.split.3x3",
|
|
1384
|
+
title: "Spatial Tiling",
|
|
1385
|
+
detail: "The layout grid mirrors the screen positions used by the menu bar app."
|
|
1386
|
+
)
|
|
1387
|
+
shortcutFactCard(
|
|
1388
|
+
icon: "terminal",
|
|
1389
|
+
title: "tmux Muscle Memory",
|
|
1390
|
+
detail: "Keep the core pane controls visible here while you tune the app-level shortcuts."
|
|
1391
|
+
)
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// MARK: - Shortcuts: App
|
|
1397
|
+
|
|
1398
|
+
private var shortcutsAppCard: some View {
|
|
1399
|
+
shortcutSectionCard(
|
|
1400
|
+
title: "App & Workspace",
|
|
1401
|
+
eyebrow: "Global",
|
|
1402
|
+
summary: "Commands for opening primary surfaces and navigating the desktop companion."
|
|
1403
|
+
) {
|
|
1404
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
900
1405
|
ForEach(HotkeyAction.allCases.filter { $0.group == .app }, id: \.rawValue) { action in
|
|
901
1406
|
compactKeyRecorder(action: action)
|
|
902
1407
|
}
|
|
903
1408
|
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
904
1411
|
|
|
905
|
-
|
|
1412
|
+
// MARK: - Shortcuts: Layers
|
|
906
1413
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1414
|
+
private var shortcutsLayersCard: some View {
|
|
1415
|
+
shortcutSectionCard(
|
|
1416
|
+
title: "Layers",
|
|
1417
|
+
eyebrow: "Workspace Stack",
|
|
1418
|
+
summary: "Direct jumps stay grouped separately from layer cycling so the numeric map is easier to scan."
|
|
1419
|
+
) {
|
|
1420
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
1421
|
+
shortcutSubsectionLabel("Jump to a Layer")
|
|
1422
|
+
|
|
1423
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1424
|
+
ForEach(HotkeyAction.layerActions, id: \.rawValue) { action in
|
|
1425
|
+
compactKeyRecorder(action: action)
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
cardDivider
|
|
1430
|
+
|
|
1431
|
+
shortcutSubsectionLabel("Cycle & Tag")
|
|
1432
|
+
|
|
1433
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1434
|
+
ForEach([HotkeyAction.layerPrev, .layerNext, .layerTag], id: \.rawValue) { action in
|
|
1435
|
+
compactKeyRecorder(action: action)
|
|
1436
|
+
}
|
|
910
1437
|
}
|
|
911
1438
|
}
|
|
912
1439
|
}
|
|
913
1440
|
}
|
|
914
1441
|
|
|
915
|
-
// MARK: - Shortcuts:
|
|
1442
|
+
// MARK: - Shortcuts: Tiling
|
|
916
1443
|
|
|
917
|
-
private var
|
|
1444
|
+
private var shortcutsTilingVisualizer: some View {
|
|
918
1445
|
VStack(alignment: .leading, spacing: 12) {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1446
|
+
shortcutSubsectionLabel("Screen Regions")
|
|
1447
|
+
|
|
1448
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
1449
|
+
VStack(spacing: 2) {
|
|
1450
|
+
HStack(spacing: 2) {
|
|
1451
|
+
tileCell(action: .tileTopLeft, label: "TL")
|
|
1452
|
+
tileCell(action: .tileTop, label: "Top")
|
|
1453
|
+
tileCell(action: .tileTopRight, label: "TR")
|
|
1454
|
+
}
|
|
1455
|
+
HStack(spacing: 2) {
|
|
1456
|
+
tileCell(action: .tileLeft, label: "Left")
|
|
1457
|
+
tileCell(action: .tileMaximize, label: "Max")
|
|
1458
|
+
tileCell(action: .tileRight, label: "Right")
|
|
1459
|
+
}
|
|
1460
|
+
HStack(spacing: 2) {
|
|
1461
|
+
tileCell(action: .tileBottomLeft, label: "BL")
|
|
1462
|
+
tileCell(action: .tileBottom, label: "Bottom")
|
|
1463
|
+
tileCell(action: .tileBottomRight, label: "BR")
|
|
1464
|
+
}
|
|
927
1465
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1466
|
+
.padding(8)
|
|
1467
|
+
.background(shortcutsInsetPanel)
|
|
1468
|
+
|
|
1469
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
1470
|
+
Text("Thirds")
|
|
1471
|
+
.font(Typo.caption(10.5))
|
|
1472
|
+
.foregroundColor(Palette.textMuted)
|
|
1473
|
+
|
|
1474
|
+
HStack(spacing: 2) {
|
|
1475
|
+
tileCell(action: .tileLeftThird, label: "\u{2153}L")
|
|
1476
|
+
tileCell(action: .tileCenterThird, label: "\u{2153}C")
|
|
1477
|
+
tileCell(action: .tileRightThird, label: "\u{2153}R")
|
|
1478
|
+
}
|
|
932
1479
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1480
|
+
.padding(8)
|
|
1481
|
+
.background(shortcutsInsetPanel)
|
|
1482
|
+
|
|
1483
|
+
Text("Use the grid as a visual legend for where each shortcut will place the focused window.")
|
|
1484
|
+
.font(Typo.caption(10.5))
|
|
1485
|
+
.foregroundColor(Palette.textMuted)
|
|
1486
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private var shortcutsTilingEditors: some View {
|
|
1492
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
1493
|
+
shortcutSubsectionLabel("Editable Bindings")
|
|
1494
|
+
|
|
1495
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1496
|
+
ForEach([
|
|
1497
|
+
HotkeyAction.tileLeft, .tileRight, .tileTop, .tileBottom,
|
|
1498
|
+
.tileTopLeft, .tileTopRight, .tileBottomLeft, .tileBottomRight
|
|
1499
|
+
], id: \.rawValue) { action in
|
|
1500
|
+
compactKeyRecorder(action: action)
|
|
937
1501
|
}
|
|
938
1502
|
}
|
|
939
|
-
.padding(6)
|
|
940
|
-
.background(
|
|
941
|
-
RoundedRectangle(cornerRadius: 6)
|
|
942
|
-
.fill(Color.black.opacity(0.25))
|
|
943
|
-
.overlay(
|
|
944
|
-
RoundedRectangle(cornerRadius: 6)
|
|
945
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
946
|
-
)
|
|
947
|
-
)
|
|
948
1503
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1504
|
+
cardDivider
|
|
1505
|
+
|
|
1506
|
+
shortcutSubsectionLabel("Layout Helpers")
|
|
1507
|
+
|
|
1508
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1509
|
+
ForEach([
|
|
1510
|
+
HotkeyAction.tileLeftThird, .tileCenterThird, .tileRightThird,
|
|
1511
|
+
.tileCenter, .tileMaximize, .tileDistribute, .tileTypeGrid
|
|
1512
|
+
], id: \.rawValue) { action in
|
|
1513
|
+
compactKeyRecorder(action: action)
|
|
1514
|
+
}
|
|
954
1515
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
RoundedRectangle(cornerRadius: 6)
|
|
958
|
-
.fill(Color.black.opacity(0.25))
|
|
959
|
-
.overlay(
|
|
960
|
-
RoundedRectangle(cornerRadius: 6)
|
|
961
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
962
|
-
)
|
|
963
|
-
)
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
964
1518
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1519
|
+
// MARK: - Shortcuts: tmux
|
|
1520
|
+
|
|
1521
|
+
private var shortcutsTmuxCard: some View {
|
|
1522
|
+
shortcutSectionCard(
|
|
1523
|
+
title: "Inside tmux",
|
|
1524
|
+
eyebrow: "Reference",
|
|
1525
|
+
summary: "These are tmux-native controls. They are shown here for fast recall and are not edited by the app."
|
|
1526
|
+
) {
|
|
1527
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
1528
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1529
|
+
shortcutRow("Detach", keys: ["Ctrl+B", "D"])
|
|
1530
|
+
shortcutRow("Kill pane", keys: ["Ctrl+B", "X"])
|
|
1531
|
+
shortcutRow("Pane left", keys: ["Ctrl+B", "\u{2190}"])
|
|
1532
|
+
shortcutRow("Pane right", keys: ["Ctrl+B", "\u{2192}"])
|
|
1533
|
+
shortcutRow("Zoom toggle", keys: ["Ctrl+B", "Z"])
|
|
1534
|
+
shortcutRow("Scroll mode", keys: ["Ctrl+B", "["])
|
|
1535
|
+
}
|
|
1536
|
+
.padding(12)
|
|
1537
|
+
.background(shortcutsInsetPanel)
|
|
1538
|
+
|
|
1539
|
+
Text("Tip: use this as your quick memory jogger while editing the global shortcuts above.")
|
|
1540
|
+
.font(Typo.caption(10.5))
|
|
1541
|
+
.foregroundColor(Palette.textMuted)
|
|
1542
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
969
1543
|
}
|
|
970
1544
|
}
|
|
971
1545
|
}
|
|
972
1546
|
|
|
973
|
-
// MARK: -
|
|
1547
|
+
// MARK: - Shortcut section UI
|
|
974
1548
|
|
|
975
|
-
private
|
|
976
|
-
|
|
977
|
-
|
|
1549
|
+
private func shortcutSectionCard<Content: View>(
|
|
1550
|
+
title: String,
|
|
1551
|
+
eyebrow: String,
|
|
1552
|
+
summary: String,
|
|
1553
|
+
@ViewBuilder content: () -> Content
|
|
1554
|
+
) -> some View {
|
|
1555
|
+
settingsCard {
|
|
1556
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
1557
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
1558
|
+
Text(eyebrow.uppercased())
|
|
1559
|
+
.font(Typo.pixel(12))
|
|
1560
|
+
.foregroundColor(Palette.textDim)
|
|
1561
|
+
.tracking(1)
|
|
978
1562
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1563
|
+
Text(title)
|
|
1564
|
+
.font(Typo.monoBold(12))
|
|
1565
|
+
.foregroundColor(Palette.text)
|
|
1566
|
+
|
|
1567
|
+
Text(summary)
|
|
1568
|
+
.font(Typo.caption(10.5))
|
|
1569
|
+
.foregroundColor(Palette.textMuted)
|
|
1570
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
content()
|
|
986
1574
|
}
|
|
987
1575
|
}
|
|
988
1576
|
}
|
|
989
1577
|
|
|
990
|
-
|
|
1578
|
+
private func shortcutFactCard(icon: String, title: String, detail: String) -> some View {
|
|
1579
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1580
|
+
Image(systemName: icon)
|
|
1581
|
+
.font(.system(size: 12, weight: .semibold))
|
|
1582
|
+
.foregroundColor(Palette.textDim)
|
|
1583
|
+
|
|
1584
|
+
Text(title)
|
|
1585
|
+
.font(Typo.monoBold(11))
|
|
1586
|
+
.foregroundColor(Palette.text)
|
|
991
1587
|
|
|
992
|
-
|
|
1588
|
+
Text(detail)
|
|
1589
|
+
.font(Typo.caption(10))
|
|
1590
|
+
.foregroundColor(Palette.textMuted)
|
|
1591
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1592
|
+
}
|
|
1593
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1594
|
+
.padding(12)
|
|
1595
|
+
.background(shortcutsInsetPanel)
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
private func shortcutSubsectionLabel(_ title: String) -> some View {
|
|
993
1599
|
Text(title.uppercased())
|
|
994
|
-
.font(Typo.pixel(
|
|
1600
|
+
.font(Typo.pixel(11))
|
|
995
1601
|
.foregroundColor(Palette.textDim)
|
|
996
1602
|
.tracking(1)
|
|
997
1603
|
}
|
|
998
1604
|
|
|
1605
|
+
private var shortcutsInsetPanel: some View {
|
|
1606
|
+
RoundedRectangle(cornerRadius: 8)
|
|
1607
|
+
.fill(Color.black.opacity(0.22))
|
|
1608
|
+
.overlay(
|
|
1609
|
+
RoundedRectangle(cornerRadius: 8)
|
|
1610
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1611
|
+
)
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
private func relativeTimestamp(_ date: Date) -> String {
|
|
1615
|
+
RelativeDateTimeFormatter().localizedString(for: date, relativeTo: Date())
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
private func companionTrustedDevices(revision: Int) -> [DeckTrustedDeviceSummary] {
|
|
1619
|
+
_ = revision
|
|
1620
|
+
return LatticesCompanionSecurityCoordinator.shared.trustedDeviceSummaries()
|
|
1621
|
+
}
|
|
1622
|
+
|
|
999
1623
|
// MARK: - Tile cell (spatial grid item)
|
|
1000
1624
|
|
|
1001
1625
|
private func tileCell(action: HotkeyAction, label: String) -> some View {
|
|
@@ -1036,6 +1660,8 @@ struct SettingsContentView: View {
|
|
|
1036
1660
|
@State private var collapsedOcrApps: Set<String> = []
|
|
1037
1661
|
|
|
1038
1662
|
@State private var activeTilePopover: HotkeyAction?
|
|
1663
|
+
@State private var selectedCompanionCockpitPageID = "main"
|
|
1664
|
+
@State private var companionTrustRevision = 0
|
|
1039
1665
|
|
|
1040
1666
|
private func tileCellPopoverBinding(for action: HotkeyAction) -> Binding<Bool> {
|
|
1041
1667
|
Binding(
|
|
@@ -1050,6 +1676,83 @@ struct SettingsContentView: View {
|
|
|
1050
1676
|
KeyRecorderView(action: action, store: hotkeyStore)
|
|
1051
1677
|
}
|
|
1052
1678
|
|
|
1679
|
+
private func companionCockpitSlotMenu(
|
|
1680
|
+
pageID: String,
|
|
1681
|
+
index: Int,
|
|
1682
|
+
shortcutID: String,
|
|
1683
|
+
categories: [LatticesCompanionShortcutCategory]
|
|
1684
|
+
) -> some View {
|
|
1685
|
+
let definition = LatticesCompanionCockpitCatalog.definition(for: shortcutID)
|
|
1686
|
+
let label = definition?.title ?? "Empty"
|
|
1687
|
+
let subtitle = definition?.subtitle ?? "Choose a shortcut"
|
|
1688
|
+
let icon = definition?.iconSystemName ?? "square.dashed"
|
|
1689
|
+
|
|
1690
|
+
return Menu {
|
|
1691
|
+
Button("Empty Slot") {
|
|
1692
|
+
prefs.updateCompanionCockpitSlot(pageID: pageID, index: index, shortcutID: "")
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
ForEach(categories) { category in
|
|
1696
|
+
let shortcuts = LatticesCompanionCockpitCatalog.shortcuts.filter {
|
|
1697
|
+
$0.category == category && !$0.id.isEmpty
|
|
1698
|
+
}
|
|
1699
|
+
if !shortcuts.isEmpty {
|
|
1700
|
+
Section(category.title) {
|
|
1701
|
+
ForEach(shortcuts) { shortcut in
|
|
1702
|
+
Button {
|
|
1703
|
+
prefs.updateCompanionCockpitSlot(
|
|
1704
|
+
pageID: pageID,
|
|
1705
|
+
index: index,
|
|
1706
|
+
shortcutID: shortcut.id
|
|
1707
|
+
)
|
|
1708
|
+
} label: {
|
|
1709
|
+
Label(shortcut.title, systemImage: shortcut.iconSystemName)
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
} label: {
|
|
1716
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
1717
|
+
HStack(alignment: .top) {
|
|
1718
|
+
Text("Slot \(index + 1)")
|
|
1719
|
+
.font(Typo.pixel(10))
|
|
1720
|
+
.foregroundColor(Palette.textDim)
|
|
1721
|
+
Spacer(minLength: 0)
|
|
1722
|
+
Image(systemName: "chevron.up.chevron.down")
|
|
1723
|
+
.font(.system(size: 10, weight: .semibold))
|
|
1724
|
+
.foregroundColor(Palette.textMuted)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
Image(systemName: icon)
|
|
1728
|
+
.font(.system(size: 14, weight: .semibold))
|
|
1729
|
+
.foregroundColor(Palette.textDim)
|
|
1730
|
+
|
|
1731
|
+
Text(label)
|
|
1732
|
+
.font(Typo.monoBold(11))
|
|
1733
|
+
.foregroundColor(Palette.text)
|
|
1734
|
+
.lineLimit(2)
|
|
1735
|
+
|
|
1736
|
+
Text(subtitle)
|
|
1737
|
+
.font(Typo.caption(9.5))
|
|
1738
|
+
.foregroundColor(Palette.textMuted)
|
|
1739
|
+
.lineLimit(3)
|
|
1740
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1741
|
+
}
|
|
1742
|
+
.frame(maxWidth: .infinity, minHeight: 112, alignment: .topLeading)
|
|
1743
|
+
.padding(10)
|
|
1744
|
+
.background(
|
|
1745
|
+
RoundedRectangle(cornerRadius: 8)
|
|
1746
|
+
.fill(Palette.surface)
|
|
1747
|
+
.overlay(
|
|
1748
|
+
RoundedRectangle(cornerRadius: 8)
|
|
1749
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1750
|
+
)
|
|
1751
|
+
)
|
|
1752
|
+
}
|
|
1753
|
+
.buttonStyle(.plain)
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1053
1756
|
// MARK: - Shortcut row (read-only, for tmux)
|
|
1054
1757
|
|
|
1055
1758
|
private func shortcutRow(_ label: String, keys: [String]) -> some View {
|
|
@@ -1137,6 +1840,9 @@ struct SettingsContentView: View {
|
|
|
1137
1840
|
HStack(spacing: 8) {
|
|
1138
1841
|
docsLinkButton(icon: "doc.text", label: "Config format", file: "config.md")
|
|
1139
1842
|
docsLinkButton(icon: "book", label: "Full concepts", file: "concepts.md")
|
|
1843
|
+
footerActionButton(icon: "stethoscope", label: "Diagnostics") {
|
|
1844
|
+
DiagnosticWindow.shared.show()
|
|
1845
|
+
}
|
|
1140
1846
|
}
|
|
1141
1847
|
.padding(.horizontal, 20)
|
|
1142
1848
|
.padding(.vertical, 12)
|
|
@@ -1176,27 +1882,38 @@ struct SettingsContentView: View {
|
|
|
1176
1882
|
let path = resolveDocsFile(file)
|
|
1177
1883
|
NSWorkspace.shared.open(URL(fileURLWithPath: path))
|
|
1178
1884
|
} label: {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
.padding(.vertical, 6)
|
|
1188
|
-
.background(
|
|
1189
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1190
|
-
.fill(Palette.surface)
|
|
1191
|
-
.overlay(
|
|
1192
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1193
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1194
|
-
)
|
|
1195
|
-
)
|
|
1885
|
+
footerActionLabel(icon: icon, label: label)
|
|
1886
|
+
}
|
|
1887
|
+
.buttonStyle(.plain)
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
private func footerActionButton(icon: String, label: String, action: @escaping () -> Void) -> some View {
|
|
1891
|
+
Button(action: action) {
|
|
1892
|
+
footerActionLabel(icon: icon, label: label)
|
|
1196
1893
|
}
|
|
1197
1894
|
.buttonStyle(.plain)
|
|
1198
1895
|
}
|
|
1199
1896
|
|
|
1897
|
+
private func footerActionLabel(icon: String, label: String) -> some View {
|
|
1898
|
+
HStack(spacing: 6) {
|
|
1899
|
+
Image(systemName: icon)
|
|
1900
|
+
.font(.system(size: 10))
|
|
1901
|
+
Text(label)
|
|
1902
|
+
.font(Typo.caption(11))
|
|
1903
|
+
}
|
|
1904
|
+
.foregroundColor(Palette.textDim)
|
|
1905
|
+
.padding(.horizontal, 12)
|
|
1906
|
+
.padding(.vertical, 6)
|
|
1907
|
+
.background(
|
|
1908
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1909
|
+
.fill(Palette.surface)
|
|
1910
|
+
.overlay(
|
|
1911
|
+
RoundedRectangle(cornerRadius: 3)
|
|
1912
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1913
|
+
)
|
|
1914
|
+
)
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1200
1917
|
private func resolveDocsFile(_ file: String) -> String {
|
|
1201
1918
|
let bundle = Bundle.main.bundlePath
|
|
1202
1919
|
let appDir = (bundle as NSString).deletingLastPathComponent
|