@lattices/cli 0.4.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/apps/mac/Info.plist +4 -4
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +5 -5
- package/docs/voice.md +11 -27
- package/package.json +11 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,3163 +0,0 @@
|
|
|
1
|
-
import DeckKit
|
|
2
|
-
import SwiftUI
|
|
3
|
-
|
|
4
|
-
/// Settings content with internal General / Shortcuts tabs.
|
|
5
|
-
/// Can also render the Docs page when `page == .docs`.
|
|
6
|
-
struct SettingsContentView: View {
|
|
7
|
-
private enum SettingsSection: String, CaseIterable, Identifiable {
|
|
8
|
-
case general
|
|
9
|
-
case shortcuts
|
|
10
|
-
case ai
|
|
11
|
-
case search
|
|
12
|
-
case companion
|
|
13
|
-
|
|
14
|
-
var id: String { rawValue }
|
|
15
|
-
|
|
16
|
-
var title: String {
|
|
17
|
-
switch self {
|
|
18
|
-
case .general: return "General"
|
|
19
|
-
case .shortcuts: return "Shortcuts"
|
|
20
|
-
case .ai: return "AI"
|
|
21
|
-
case .search: return "Search & OCR"
|
|
22
|
-
case .companion: return "LATS iOS Companion"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
var icon: String {
|
|
27
|
-
switch self {
|
|
28
|
-
case .general: return "slider.horizontal.3"
|
|
29
|
-
case .shortcuts: return "command"
|
|
30
|
-
case .ai: return "sparkles"
|
|
31
|
-
case .search: return "text.viewfinder"
|
|
32
|
-
case .companion: return "ipad.and.iphone"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
var eyebrow: String {
|
|
37
|
-
switch self {
|
|
38
|
-
case .general: return "Workspace"
|
|
39
|
-
case .shortcuts: return "Controls"
|
|
40
|
-
case .ai: return "Agents"
|
|
41
|
-
case .search: return "Indexing"
|
|
42
|
-
case .companion: return "Local Bridge"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
var summary: String {
|
|
47
|
-
switch self {
|
|
48
|
-
case .general:
|
|
49
|
-
return "App updates, permissions, terminal defaults, project discovery, and interaction behavior."
|
|
50
|
-
case .shortcuts:
|
|
51
|
-
return "A full map of global hotkeys for workspace movement and tmux flow."
|
|
52
|
-
case .ai:
|
|
53
|
-
return "Claude CLI detection plus advisor model and spending controls."
|
|
54
|
-
case .search:
|
|
55
|
-
return "OCR cadence, quality, and recent capture visibility."
|
|
56
|
-
case .companion:
|
|
57
|
-
return "Local-network pairing, trusted iPad devices, and bridge security."
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
var page: AppPage = .settings
|
|
63
|
-
@ObservedObject var prefs: Preferences
|
|
64
|
-
@ObservedObject var scanner: ProjectScanner
|
|
65
|
-
@ObservedObject var hotkeyStore: HotkeyStore = .shared
|
|
66
|
-
@ObservedObject var workspaceManager: WorkspaceManager = .shared
|
|
67
|
-
@ObservedObject var appUpdater: AppUpdater = .shared
|
|
68
|
-
@ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
|
|
69
|
-
@ObservedObject var keyboardRemapStore: KeyboardRemapStore = .shared
|
|
70
|
-
@ObservedObject var permChecker: PermissionChecker = .shared
|
|
71
|
-
@ObservedObject var mouseGestureController: MouseGestureController = .shared
|
|
72
|
-
@ObservedObject var keyboardRemapController: KeyboardRemapController = .shared
|
|
73
|
-
var onBack: (() -> Void)? = nil
|
|
74
|
-
|
|
75
|
-
@State private var selectedTab: SettingsSection = .general
|
|
76
|
-
|
|
77
|
-
var body: some View {
|
|
78
|
-
VStack(spacing: 0) {
|
|
79
|
-
// Back bar
|
|
80
|
-
backBar
|
|
81
|
-
|
|
82
|
-
if page == .docs {
|
|
83
|
-
docsContent
|
|
84
|
-
} else {
|
|
85
|
-
settingsBody
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
89
|
-
.clipped()
|
|
90
|
-
.background(PanelBackground())
|
|
91
|
-
.onAppear {
|
|
92
|
-
permChecker.check()
|
|
93
|
-
if page == .companionSettings {
|
|
94
|
-
selectedTab = .companion
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
.onReceive(NotificationCenter.default.publisher(for: .latticesShowGeneralSettings)) { _ in
|
|
98
|
-
selectedTab = .general
|
|
99
|
-
}
|
|
100
|
-
.onReceive(NotificationCenter.default.publisher(for: .latticesShowAssistantSettings)) { _ in
|
|
101
|
-
selectedTab = .ai
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// MARK: - Back Bar
|
|
106
|
-
|
|
107
|
-
private var currentTabLabel: String {
|
|
108
|
-
page == .docs ? "Docs" : selectedTab.title
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private var snapModifierBinding: Binding<SnapModifierKey> {
|
|
112
|
-
Binding(
|
|
113
|
-
get: { workspaceManager.snapZonesConfig.modifier ?? .command },
|
|
114
|
-
set: { workspaceManager.updateSnapModifier($0) }
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private var backBar: some View {
|
|
119
|
-
VStack(spacing: 0) {
|
|
120
|
-
HStack(spacing: 8) {
|
|
121
|
-
if let onBack {
|
|
122
|
-
Button {
|
|
123
|
-
onBack()
|
|
124
|
-
} label: {
|
|
125
|
-
Image(systemName: "chevron.left")
|
|
126
|
-
.font(.system(size: 10, weight: .semibold))
|
|
127
|
-
.foregroundColor(Palette.textMuted)
|
|
128
|
-
}
|
|
129
|
-
.buttonStyle(.plain)
|
|
130
|
-
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
Text(page == .docs ? "Docs" : currentTabLabel)
|
|
134
|
-
.font(Typo.heading(13))
|
|
135
|
-
.foregroundColor(Palette.text)
|
|
136
|
-
|
|
137
|
-
Spacer()
|
|
138
|
-
}
|
|
139
|
-
.padding(.horizontal, 16)
|
|
140
|
-
.padding(.vertical, 8)
|
|
141
|
-
|
|
142
|
-
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// MARK: - Settings Body
|
|
147
|
-
|
|
148
|
-
private var settingsBody: some View {
|
|
149
|
-
HStack(spacing: 0) {
|
|
150
|
-
settingsSidebar
|
|
151
|
-
.frame(width: 190, alignment: .top)
|
|
152
|
-
|
|
153
|
-
Rectangle()
|
|
154
|
-
.fill(Palette.border)
|
|
155
|
-
.frame(width: 0.5)
|
|
156
|
-
.frame(maxHeight: .infinity)
|
|
157
|
-
|
|
158
|
-
VStack(spacing: 0) {
|
|
159
|
-
settingsSectionHero(selectedTab)
|
|
160
|
-
|
|
161
|
-
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
162
|
-
|
|
163
|
-
selectedSectionContent
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private var settingsSidebar: some View {
|
|
169
|
-
VStack(alignment: .leading, spacing: 14) {
|
|
170
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
171
|
-
Text("SETTINGS")
|
|
172
|
-
.font(Typo.pixel(14))
|
|
173
|
-
.foregroundColor(Palette.textDim)
|
|
174
|
-
.tracking(1)
|
|
175
|
-
Text("Tune how Lattices launches workspaces, listens for commands, and navigates the desktop.")
|
|
176
|
-
.font(Typo.caption(11))
|
|
177
|
-
.foregroundColor(Palette.textMuted)
|
|
178
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
VStack(spacing: 6) {
|
|
182
|
-
ForEach(SettingsSection.allCases) { section in
|
|
183
|
-
settingsTab(section)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
Spacer(minLength: 0)
|
|
188
|
-
}
|
|
189
|
-
.padding(16)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private func settingsTab(_ section: SettingsSection) -> some View {
|
|
193
|
-
let active = selectedTab == section
|
|
194
|
-
return Button {
|
|
195
|
-
selectedTab = section
|
|
196
|
-
} label: {
|
|
197
|
-
HStack(alignment: .top, spacing: 10) {
|
|
198
|
-
Image(systemName: section.icon)
|
|
199
|
-
.font(.system(size: 11, weight: .semibold))
|
|
200
|
-
.foregroundColor(active ? Palette.text : Palette.textMuted)
|
|
201
|
-
.frame(width: 16, height: 18, alignment: .center)
|
|
202
|
-
|
|
203
|
-
VStack(alignment: .leading, spacing: 3) {
|
|
204
|
-
Text(section.title)
|
|
205
|
-
.font(Typo.mono(11))
|
|
206
|
-
.foregroundColor(active ? Palette.text : Palette.textMuted)
|
|
207
|
-
|
|
208
|
-
Text(section.eyebrow)
|
|
209
|
-
.font(Typo.caption(9))
|
|
210
|
-
.foregroundColor(Palette.textMuted.opacity(active ? 0.85 : 0.62))
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
Spacer(minLength: 0)
|
|
214
|
-
}
|
|
215
|
-
.padding(.horizontal, 10)
|
|
216
|
-
.padding(.vertical, 8)
|
|
217
|
-
.contentShape(RoundedRectangle(cornerRadius: 8))
|
|
218
|
-
.background(
|
|
219
|
-
ZStack {
|
|
220
|
-
if active {
|
|
221
|
-
RoundedRectangle(cornerRadius: 8)
|
|
222
|
-
.fill(Color.white.opacity(0.06))
|
|
223
|
-
RoundedRectangle(cornerRadius: 8)
|
|
224
|
-
.strokeBorder(
|
|
225
|
-
LinearGradient(
|
|
226
|
-
colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
|
|
227
|
-
startPoint: .top,
|
|
228
|
-
endPoint: .bottom
|
|
229
|
-
),
|
|
230
|
-
lineWidth: 0.5
|
|
231
|
-
)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
.buttonStyle(.plain)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private func settingsSectionHero(_ section: SettingsSection) -> some View {
|
|
240
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
241
|
-
Text(section.eyebrow.uppercased())
|
|
242
|
-
.font(Typo.pixel(14))
|
|
243
|
-
.foregroundColor(Palette.textDim)
|
|
244
|
-
.tracking(1)
|
|
245
|
-
|
|
246
|
-
Text(section.title)
|
|
247
|
-
.font(Typo.heading(16))
|
|
248
|
-
.foregroundColor(Palette.text)
|
|
249
|
-
|
|
250
|
-
Text(section.summary)
|
|
251
|
-
.font(Typo.caption(11))
|
|
252
|
-
.foregroundColor(Palette.textMuted)
|
|
253
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
254
|
-
}
|
|
255
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
256
|
-
.padding(.horizontal, 20)
|
|
257
|
-
.padding(.vertical, 14)
|
|
258
|
-
.background(Palette.bg)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
@ViewBuilder
|
|
262
|
-
private var selectedSectionContent: some View {
|
|
263
|
-
switch selectedTab {
|
|
264
|
-
case .general:
|
|
265
|
-
generalContent
|
|
266
|
-
case .shortcuts:
|
|
267
|
-
shortcutsContent
|
|
268
|
-
case .ai:
|
|
269
|
-
aiContent
|
|
270
|
-
case .search:
|
|
271
|
-
searchOcrContent
|
|
272
|
-
case .companion:
|
|
273
|
-
companionContent
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// MARK: - Sticky section header
|
|
278
|
-
|
|
279
|
-
private func stickyHeader(_ title: String) -> some View {
|
|
280
|
-
VStack(spacing: 0) {
|
|
281
|
-
HStack {
|
|
282
|
-
Text(title.uppercased())
|
|
283
|
-
.font(Typo.pixel(14))
|
|
284
|
-
.foregroundColor(Palette.textDim)
|
|
285
|
-
.tracking(1)
|
|
286
|
-
Spacer()
|
|
287
|
-
}
|
|
288
|
-
.padding(.horizontal, 20)
|
|
289
|
-
.padding(.vertical, 8)
|
|
290
|
-
.background(Palette.bg)
|
|
291
|
-
|
|
292
|
-
Rectangle()
|
|
293
|
-
.fill(Palette.border)
|
|
294
|
-
.frame(height: 0.5)
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// MARK: - General
|
|
299
|
-
|
|
300
|
-
private var permissionsAssistantCard: some View {
|
|
301
|
-
let missing = Capability.allCases.filter { !$0.isGranted }
|
|
302
|
-
return settingsCard {
|
|
303
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
304
|
-
HStack(alignment: .center, spacing: 8) {
|
|
305
|
-
RoundedRectangle(cornerRadius: 5)
|
|
306
|
-
.fill((missing.isEmpty ? Palette.running : Palette.detach).opacity(0.13))
|
|
307
|
-
.overlay(
|
|
308
|
-
Image(systemName: missing.isEmpty ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
|
|
309
|
-
.font(.system(size: 12, weight: .semibold))
|
|
310
|
-
.foregroundColor(missing.isEmpty ? Palette.running : Palette.detach)
|
|
311
|
-
)
|
|
312
|
-
.frame(width: 26, height: 26)
|
|
313
|
-
|
|
314
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
315
|
-
Text("Permissions")
|
|
316
|
-
.font(Typo.mono(12))
|
|
317
|
-
.foregroundColor(Palette.text)
|
|
318
|
-
Text(missing.isEmpty ? "Ready for window control, gestures, and OCR" : "\(missing.count) permission \(missing.count == 1 ? "needs" : "need") attention")
|
|
319
|
-
.font(Typo.caption(9.5))
|
|
320
|
-
.foregroundColor(Palette.textMuted)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
Spacer()
|
|
324
|
-
|
|
325
|
-
Text(missing.isEmpty ? "All on" : "\(missing.count) off")
|
|
326
|
-
.font(Typo.monoBold(9.5))
|
|
327
|
-
.foregroundColor(missing.isEmpty ? Palette.running : Palette.detach)
|
|
328
|
-
.padding(.horizontal, 7)
|
|
329
|
-
.padding(.vertical, 3)
|
|
330
|
-
.background(
|
|
331
|
-
Capsule()
|
|
332
|
-
.fill((missing.isEmpty ? Palette.running : Palette.detach).opacity(0.10))
|
|
333
|
-
.overlay(Capsule().strokeBorder((missing.isEmpty ? Palette.running : Palette.detach).opacity(0.18), lineWidth: 0.5))
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
Text("The assistant explains each macOS prompt before you grant it. Advanced privacy panes stay available for review when a synthetic shortcut or input capture needs macOS-level repair.")
|
|
338
|
-
.font(Typo.caption(10))
|
|
339
|
-
.foregroundColor(Palette.textMuted)
|
|
340
|
-
|
|
341
|
-
HStack(spacing: 8) {
|
|
342
|
-
ForEach(Capability.allCases) { cap in
|
|
343
|
-
HStack(spacing: 4) {
|
|
344
|
-
Circle()
|
|
345
|
-
.fill(cap.isGranted ? Palette.running : Palette.detach)
|
|
346
|
-
.frame(width: 5, height: 5)
|
|
347
|
-
Text(cap.title)
|
|
348
|
-
.font(Typo.mono(9))
|
|
349
|
-
.foregroundColor(Palette.textMuted)
|
|
350
|
-
}
|
|
351
|
-
.padding(.horizontal, 6)
|
|
352
|
-
.padding(.vertical, 2)
|
|
353
|
-
.background(
|
|
354
|
-
Capsule().fill(Palette.surface)
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
Spacer(minLength: 0)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
HStack(spacing: 8) {
|
|
361
|
-
Button {
|
|
362
|
-
PermissionsAssistantWindowController.shared.show(focus: missing.first)
|
|
363
|
-
} label: {
|
|
364
|
-
Text(missing.isEmpty ? "Open Assistant" : "Set Up")
|
|
365
|
-
.font(Typo.monoBold(10))
|
|
366
|
-
.foregroundColor(Palette.text)
|
|
367
|
-
.padding(.horizontal, 12)
|
|
368
|
-
.padding(.vertical, 5)
|
|
369
|
-
.background(
|
|
370
|
-
RoundedRectangle(cornerRadius: 4)
|
|
371
|
-
.fill(Palette.surfaceHov)
|
|
372
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
373
|
-
)
|
|
374
|
-
}
|
|
375
|
-
.buttonStyle(.plain)
|
|
376
|
-
|
|
377
|
-
Button {
|
|
378
|
-
permChecker.check()
|
|
379
|
-
} label: {
|
|
380
|
-
Image(systemName: "arrow.clockwise")
|
|
381
|
-
.font(.system(size: 10, weight: .semibold))
|
|
382
|
-
.foregroundColor(Palette.textDim)
|
|
383
|
-
.frame(width: 24, height: 22)
|
|
384
|
-
}
|
|
385
|
-
.buttonStyle(.plain)
|
|
386
|
-
.help("Refresh permission status")
|
|
387
|
-
|
|
388
|
-
Spacer()
|
|
389
|
-
|
|
390
|
-
Button {
|
|
391
|
-
permChecker.openAutomationSettings()
|
|
392
|
-
} label: {
|
|
393
|
-
Text("Automation")
|
|
394
|
-
.font(Typo.caption(9))
|
|
395
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
396
|
-
}
|
|
397
|
-
.buttonStyle(.plain)
|
|
398
|
-
|
|
399
|
-
Button {
|
|
400
|
-
permChecker.openInputMonitoringSettings()
|
|
401
|
-
} label: {
|
|
402
|
-
Text("Input Monitoring")
|
|
403
|
-
.font(Typo.caption(9))
|
|
404
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
405
|
-
}
|
|
406
|
-
.buttonStyle(.plain)
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
private var permissionsDetailCard: some View {
|
|
413
|
-
settingsCard {
|
|
414
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
415
|
-
HStack(alignment: .center, spacing: 8) {
|
|
416
|
-
Image(systemName: permChecker.allGranted ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
|
|
417
|
-
.font(.system(size: 11, weight: .medium))
|
|
418
|
-
.foregroundColor(permChecker.allGranted ? Palette.running : Palette.detach)
|
|
419
|
-
Text("macOS permissions")
|
|
420
|
-
.font(Typo.mono(12))
|
|
421
|
-
.foregroundColor(Palette.text)
|
|
422
|
-
Spacer()
|
|
423
|
-
Button {
|
|
424
|
-
permChecker.check()
|
|
425
|
-
} label: {
|
|
426
|
-
Image(systemName: "arrow.clockwise")
|
|
427
|
-
.font(.system(size: 10, weight: .semibold))
|
|
428
|
-
.foregroundColor(Palette.textDim)
|
|
429
|
-
.frame(width: 24, height: 22)
|
|
430
|
-
}
|
|
431
|
-
.buttonStyle(.plain)
|
|
432
|
-
.help("Refresh permission status")
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
Text("Window discovery, gestures, remaps, OCR, and synthetic shortcuts all depend on these macOS grants.")
|
|
436
|
-
.font(Typo.caption(10))
|
|
437
|
-
.foregroundColor(Palette.textMuted)
|
|
438
|
-
|
|
439
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
440
|
-
permissionSettingsRow(
|
|
441
|
-
"Accessibility",
|
|
442
|
-
granted: permChecker.accessibility,
|
|
443
|
-
detail: "Required for mouse gestures, keyboard remaps, window movement, and focusing windows."
|
|
444
|
-
) {
|
|
445
|
-
permChecker.requestAccessibility()
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
permissionSettingsRow(
|
|
449
|
-
"Screen Recording",
|
|
450
|
-
granted: permChecker.screenRecording,
|
|
451
|
-
detail: "Required for reliable window titles, OCR, and Space-aware window discovery."
|
|
452
|
-
) {
|
|
453
|
-
permChecker.requestScreenRecording()
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
permissionReviewRow(
|
|
457
|
-
"Automation",
|
|
458
|
-
detail: "Needed when Lattices sends shortcuts through System Events, including gesture-triggered dictation."
|
|
459
|
-
) {
|
|
460
|
-
permChecker.openAutomationSettings()
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
permissionReviewRow(
|
|
464
|
-
"Input Monitoring",
|
|
465
|
-
detail: "Useful to review if global input capture or synthetic shortcut behavior starts failing."
|
|
466
|
-
) {
|
|
467
|
-
permChecker.openInputMonitoringSettings()
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private var appUpdateCard: some View {
|
|
475
|
-
settingsCard {
|
|
476
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
477
|
-
HStack(alignment: .center, spacing: 10) {
|
|
478
|
-
RoundedRectangle(cornerRadius: 6)
|
|
479
|
-
.fill((LatticesRuntime.isDevBuild ? Palette.detach : Palette.running).opacity(0.13))
|
|
480
|
-
.overlay(
|
|
481
|
-
Image(systemName: LatticesRuntime.isDevBuild ? "hammer.fill" : "checkmark.seal.fill")
|
|
482
|
-
.font(.system(size: 13, weight: .semibold))
|
|
483
|
-
.foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
|
|
484
|
-
)
|
|
485
|
-
.frame(width: 30, height: 30)
|
|
486
|
-
|
|
487
|
-
VStack(alignment: .leading, spacing: 4) {
|
|
488
|
-
HStack(spacing: 8) {
|
|
489
|
-
Text("Lattices app")
|
|
490
|
-
.font(Typo.mono(12))
|
|
491
|
-
.foregroundColor(Palette.text)
|
|
492
|
-
buildChannelBadge
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
496
|
-
Text(appUpdater.currentDisplayVersion)
|
|
497
|
-
.font(Typo.monoBold(13))
|
|
498
|
-
.foregroundColor(Palette.text)
|
|
499
|
-
Text(LatticesRuntime.buildStatusLabel)
|
|
500
|
-
.font(Typo.monoBold(9.5))
|
|
501
|
-
.foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
Spacer()
|
|
506
|
-
|
|
507
|
-
Toggle("Auto", isOn: $appUpdater.autoCheckEnabled)
|
|
508
|
-
.font(Typo.caption(9))
|
|
509
|
-
.toggleStyle(.checkbox)
|
|
510
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if let update = appUpdater.availableUpdate {
|
|
514
|
-
Text("New version v\(update.version) is ready")
|
|
515
|
-
.font(Typo.monoBold(10))
|
|
516
|
-
.foregroundColor(Palette.detach)
|
|
517
|
-
} else if appUpdater.isChecking {
|
|
518
|
-
Text("Checking for updates...")
|
|
519
|
-
.font(Typo.caption(9))
|
|
520
|
-
.foregroundColor(Palette.textMuted)
|
|
521
|
-
} else if let error = appUpdater.lastError {
|
|
522
|
-
Text(error)
|
|
523
|
-
.font(Typo.caption(9))
|
|
524
|
-
.foregroundColor(Palette.detach.opacity(0.9))
|
|
525
|
-
} else if let checked = appUpdater.lastChecked {
|
|
526
|
-
Text("Last checked \(checked, style: .relative)")
|
|
527
|
-
.font(Typo.caption(9))
|
|
528
|
-
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
HStack(spacing: 10) {
|
|
532
|
-
Button {
|
|
533
|
-
appUpdater.promptForUpdate()
|
|
534
|
-
} label: {
|
|
535
|
-
Text(appUpdater.isUpdating ? "Preparing..." : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
|
|
536
|
-
.font(Typo.monoBold(10))
|
|
537
|
-
.foregroundColor(Palette.text)
|
|
538
|
-
.padding(.horizontal, 12)
|
|
539
|
-
.padding(.vertical, 5)
|
|
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
|
-
.disabled(appUpdater.isUpdating)
|
|
548
|
-
|
|
549
|
-
Button {
|
|
550
|
-
Task { await appUpdater.check() }
|
|
551
|
-
} label: {
|
|
552
|
-
Text(appUpdater.isChecking ? "Checking..." : "Check Now")
|
|
553
|
-
.font(Typo.caption(9))
|
|
554
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
555
|
-
}
|
|
556
|
-
.buttonStyle(.plain)
|
|
557
|
-
.disabled(appUpdater.isChecking)
|
|
558
|
-
|
|
559
|
-
Spacer()
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
private var interactionBehaviorCard: some View {
|
|
566
|
-
settingsCard {
|
|
567
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
568
|
-
Text("Session behavior")
|
|
569
|
-
.font(Typo.mono(12))
|
|
570
|
-
.foregroundColor(Palette.text)
|
|
571
|
-
|
|
572
|
-
HStack {
|
|
573
|
-
Text("Detach mode")
|
|
574
|
-
.font(Typo.mono(10))
|
|
575
|
-
.foregroundColor(Palette.textDim)
|
|
576
|
-
Spacer()
|
|
577
|
-
Picker("", selection: $prefs.mode) {
|
|
578
|
-
Text("Learning").tag(InteractionMode.learning)
|
|
579
|
-
Text("Auto").tag(InteractionMode.auto)
|
|
580
|
-
}
|
|
581
|
-
.pickerStyle(.segmented)
|
|
582
|
-
.labelsHidden()
|
|
583
|
-
.frame(width: 160)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
Text(prefs.mode == .learning
|
|
587
|
-
? "Shows keybinding hints when you detach from a tmux session."
|
|
588
|
-
: "Detaches sessions quietly once Lattices has done the workspace handoff.")
|
|
589
|
-
.font(Typo.caption(9.5))
|
|
590
|
-
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private var inputControlsCard: some View {
|
|
596
|
-
shortcutSectionCard(
|
|
597
|
-
title: "Input Controls",
|
|
598
|
-
eyebrow: "Gestures & Remaps",
|
|
599
|
-
summary: "Mouse gestures, drag snapping, and Hyper key remaps live alongside the shortcuts they trigger."
|
|
600
|
-
) {
|
|
601
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
602
|
-
HStack(alignment: .top, spacing: 12) {
|
|
603
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
604
|
-
Text("Drag-to-snap")
|
|
605
|
-
.font(Typo.monoBold(11))
|
|
606
|
-
.foregroundColor(Palette.text)
|
|
607
|
-
Text("Hold a modifier while dragging a window to reveal snap targets.")
|
|
608
|
-
.font(Typo.caption(10))
|
|
609
|
-
.foregroundColor(Palette.textMuted)
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
Spacer()
|
|
613
|
-
|
|
614
|
-
Toggle("", isOn: $prefs.dragSnapEnabled)
|
|
615
|
-
.toggleStyle(.switch)
|
|
616
|
-
.controlSize(.small)
|
|
617
|
-
.labelsHidden()
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
HStack {
|
|
621
|
-
Text("Snap modifier")
|
|
622
|
-
.font(Typo.mono(10))
|
|
623
|
-
.foregroundColor(Palette.textDim)
|
|
624
|
-
Spacer()
|
|
625
|
-
Picker("", selection: snapModifierBinding) {
|
|
626
|
-
ForEach(SnapModifierKey.allCases) { modifier in
|
|
627
|
-
Text(modifier.shortLabel).tag(modifier)
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
.pickerStyle(.segmented)
|
|
631
|
-
.labelsHidden()
|
|
632
|
-
.frame(width: 220)
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
cardDivider
|
|
636
|
-
|
|
637
|
-
HStack(alignment: .top, spacing: 12) {
|
|
638
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
639
|
-
Text("Middle-click gestures")
|
|
640
|
-
.font(Typo.monoBold(11))
|
|
641
|
-
.foregroundColor(Palette.text)
|
|
642
|
-
Text("Directional mouse gestures can switch Spaces, open the Screen Map, or trigger dictation.")
|
|
643
|
-
.font(Typo.caption(10))
|
|
644
|
-
.foregroundColor(Palette.textMuted)
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
Spacer()
|
|
648
|
-
|
|
649
|
-
Toggle("", isOn: $prefs.mouseGesturesEnabled)
|
|
650
|
-
.toggleStyle(.switch)
|
|
651
|
-
.controlSize(.small)
|
|
652
|
-
.labelsHidden()
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
breakerStatusRow(
|
|
656
|
-
state: mouseGestureController.breakerState,
|
|
657
|
-
label: "Mouse gestures"
|
|
658
|
-
) {
|
|
659
|
-
mouseGestureController.reArmAfterBreakerTrip()
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
cardDivider
|
|
663
|
-
|
|
664
|
-
HStack(alignment: .top, spacing: 12) {
|
|
665
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
666
|
-
Text("Caps Lock as Hyper")
|
|
667
|
-
.font(Typo.monoBold(11))
|
|
668
|
-
.foregroundColor(Palette.text)
|
|
669
|
-
Text("Hold Caps Lock for Hyper shortcuts, tap it for Escape.")
|
|
670
|
-
.font(Typo.caption(10))
|
|
671
|
-
.foregroundColor(Palette.textMuted)
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
Spacer()
|
|
675
|
-
|
|
676
|
-
Toggle("", isOn: $prefs.keyboardRemapsEnabled)
|
|
677
|
-
.toggleStyle(.switch)
|
|
678
|
-
.controlSize(.small)
|
|
679
|
-
.labelsHidden()
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
breakerStatusRow(
|
|
683
|
-
state: keyboardRemapController.breakerState,
|
|
684
|
-
label: "Keyboard remaps"
|
|
685
|
-
) {
|
|
686
|
-
keyboardRemapController.reArmAfterBreakerTrip()
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
private var generalContent: some View {
|
|
693
|
-
ScrollView {
|
|
694
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
695
|
-
appUpdateCard
|
|
696
|
-
|
|
697
|
-
permissionsAssistantCard
|
|
698
|
-
|
|
699
|
-
settingsCard {
|
|
700
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
701
|
-
Text("Terminal")
|
|
702
|
-
.font(Typo.mono(11))
|
|
703
|
-
.foregroundColor(Palette.text)
|
|
704
|
-
|
|
705
|
-
Picker("", selection: $prefs.terminal) {
|
|
706
|
-
ForEach(Terminal.installed) { t in
|
|
707
|
-
Text(t.rawValue).tag(t)
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
.pickerStyle(.segmented)
|
|
711
|
-
.labelsHidden()
|
|
712
|
-
|
|
713
|
-
Text("Used for attaching to tmux sessions")
|
|
714
|
-
.font(Typo.caption(10))
|
|
715
|
-
.foregroundColor(Palette.textMuted)
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
settingsCard {
|
|
720
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
721
|
-
Text("Project discovery")
|
|
722
|
-
.font(Typo.mono(11))
|
|
723
|
-
.foregroundColor(Palette.text)
|
|
724
|
-
|
|
725
|
-
Text("Project scan root")
|
|
726
|
-
.font(Typo.mono(10))
|
|
727
|
-
.foregroundColor(Palette.textDim)
|
|
728
|
-
|
|
729
|
-
HStack(spacing: 6) {
|
|
730
|
-
TextField("~/dev", text: $prefs.scanRoot)
|
|
731
|
-
.textFieldStyle(.plain)
|
|
732
|
-
.font(Typo.mono(11))
|
|
733
|
-
.foregroundColor(Palette.text)
|
|
734
|
-
.padding(.horizontal, 8)
|
|
735
|
-
.padding(.vertical, 5)
|
|
736
|
-
.background(
|
|
737
|
-
RoundedRectangle(cornerRadius: 5)
|
|
738
|
-
.fill(Color.white.opacity(0.06))
|
|
739
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
Button {
|
|
743
|
-
let panel = NSOpenPanel()
|
|
744
|
-
panel.canChooseDirectories = true
|
|
745
|
-
panel.canChooseFiles = false
|
|
746
|
-
panel.allowsMultipleSelection = false
|
|
747
|
-
if !prefs.scanRoot.isEmpty {
|
|
748
|
-
panel.directoryURL = URL(fileURLWithPath: prefs.scanRoot)
|
|
749
|
-
}
|
|
750
|
-
if panel.runModal() == .OK, let url = panel.url {
|
|
751
|
-
prefs.scanRoot = url.path
|
|
752
|
-
}
|
|
753
|
-
} label: {
|
|
754
|
-
Image(systemName: "folder")
|
|
755
|
-
.font(.system(size: 11))
|
|
756
|
-
.foregroundColor(Palette.textDim)
|
|
757
|
-
.padding(6)
|
|
758
|
-
.background(
|
|
759
|
-
RoundedRectangle(cornerRadius: 5)
|
|
760
|
-
.fill(Color.white.opacity(0.06))
|
|
761
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
|
|
762
|
-
)
|
|
763
|
-
}
|
|
764
|
-
.buttonStyle(.plain)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
HStack {
|
|
768
|
-
Text("Scans for .lattices.json project configs")
|
|
769
|
-
.font(Typo.caption(9))
|
|
770
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
771
|
-
Spacer()
|
|
772
|
-
Button {
|
|
773
|
-
scanner.updateRoot(prefs.scanRoot)
|
|
774
|
-
scanner.scan()
|
|
775
|
-
} label: {
|
|
776
|
-
Text("Rescan")
|
|
777
|
-
.font(Typo.monoBold(10))
|
|
778
|
-
.foregroundColor(Palette.text)
|
|
779
|
-
.padding(.horizontal, 12)
|
|
780
|
-
.padding(.vertical, 4)
|
|
781
|
-
.background(
|
|
782
|
-
RoundedRectangle(cornerRadius: 4)
|
|
783
|
-
.fill(Palette.surfaceHov)
|
|
784
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
785
|
-
)
|
|
786
|
-
}
|
|
787
|
-
.buttonStyle(.plain)
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
interactionBehaviorCard
|
|
793
|
-
}
|
|
794
|
-
.padding(16)
|
|
795
|
-
.frame(maxWidth: 760, alignment: .leading)
|
|
796
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
private var permissionsContent: some View {
|
|
801
|
-
ScrollView {
|
|
802
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
803
|
-
permissionsAssistantCard
|
|
804
|
-
|
|
805
|
-
settingsCard {
|
|
806
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
807
|
-
HStack(alignment: .center, spacing: 8) {
|
|
808
|
-
Image(systemName: permChecker.allGranted ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
|
|
809
|
-
.font(.system(size: 11, weight: .medium))
|
|
810
|
-
.foregroundColor(permChecker.allGranted ? Palette.running : Palette.detach)
|
|
811
|
-
Text("Permissions")
|
|
812
|
-
.font(Typo.mono(12))
|
|
813
|
-
.foregroundColor(Palette.text)
|
|
814
|
-
Spacer()
|
|
815
|
-
Button {
|
|
816
|
-
permChecker.check()
|
|
817
|
-
} label: {
|
|
818
|
-
Image(systemName: "arrow.clockwise")
|
|
819
|
-
.font(.system(size: 10, weight: .semibold))
|
|
820
|
-
.foregroundColor(Palette.textDim)
|
|
821
|
-
.frame(width: 24, height: 22)
|
|
822
|
-
}
|
|
823
|
-
.buttonStyle(.plain)
|
|
824
|
-
.help("Refresh permission status")
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
Text("Lattices uses macOS privacy permissions for window discovery, tiling, gestures, remaps, and synthetic shortcuts.")
|
|
828
|
-
.font(Typo.caption(10))
|
|
829
|
-
.foregroundColor(Palette.textMuted)
|
|
830
|
-
|
|
831
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
832
|
-
permissionSettingsRow(
|
|
833
|
-
"Accessibility",
|
|
834
|
-
granted: permChecker.accessibility,
|
|
835
|
-
detail: "Required for mouse gestures, keyboard remaps, window movement, and focusing windows."
|
|
836
|
-
) {
|
|
837
|
-
permChecker.requestAccessibility()
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
permissionSettingsRow(
|
|
841
|
-
"Screen Recording",
|
|
842
|
-
granted: permChecker.screenRecording,
|
|
843
|
-
detail: "Required for reliable window titles, OCR, and Space-aware window discovery."
|
|
844
|
-
) {
|
|
845
|
-
permChecker.requestScreenRecording()
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
permissionReviewRow(
|
|
849
|
-
"Automation",
|
|
850
|
-
detail: "Needed when Lattices sends shortcuts through System Events, including gesture-triggered dictation."
|
|
851
|
-
) {
|
|
852
|
-
permChecker.openAutomationSettings()
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
permissionReviewRow(
|
|
856
|
-
"Input Monitoring",
|
|
857
|
-
detail: "Useful to review if global input capture or synthetic shortcut behavior starts failing."
|
|
858
|
-
) {
|
|
859
|
-
permChecker.openInputMonitoringSettings()
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
.padding(16)
|
|
866
|
-
.frame(maxWidth: 760, alignment: .leading)
|
|
867
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
private var appContent: some View {
|
|
872
|
-
ScrollView {
|
|
873
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
874
|
-
settingsCard {
|
|
875
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
876
|
-
HStack(alignment: .top, spacing: 10) {
|
|
877
|
-
Image(systemName: LatticesRuntime.isDevBuild ? "hammer.fill" : "checkmark.seal.fill")
|
|
878
|
-
.font(.system(size: 13, weight: .semibold))
|
|
879
|
-
.foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
|
|
880
|
-
.frame(width: 24, height: 24)
|
|
881
|
-
|
|
882
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
883
|
-
HStack(spacing: 8) {
|
|
884
|
-
Text("Lattices app")
|
|
885
|
-
.font(Typo.mono(12))
|
|
886
|
-
.foregroundColor(Palette.text)
|
|
887
|
-
buildChannelBadge
|
|
888
|
-
Spacer()
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
892
|
-
Text(appUpdater.currentDisplayVersion)
|
|
893
|
-
.font(Typo.heading(20))
|
|
894
|
-
.foregroundColor(Palette.text)
|
|
895
|
-
Text(LatticesRuntime.buildStatusLabel)
|
|
896
|
-
.font(Typo.monoBold(10))
|
|
897
|
-
.foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if let revision = LatticesRuntime.buildRevision {
|
|
901
|
-
Text("Build \(revision)")
|
|
902
|
-
.font(Typo.caption(9))
|
|
903
|
-
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
Text("Lattices can check for new signed releases and prepare the update here. You’ll confirm before the app quits and relaunches.")
|
|
909
|
-
.font(Typo.caption(10))
|
|
910
|
-
.foregroundColor(Palette.textMuted)
|
|
911
|
-
|
|
912
|
-
if let update = appUpdater.availableUpdate {
|
|
913
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
914
|
-
HStack(spacing: 6) {
|
|
915
|
-
Image(systemName: "gift.fill")
|
|
916
|
-
.font(.system(size: 10, weight: .semibold))
|
|
917
|
-
.foregroundColor(Palette.detach)
|
|
918
|
-
Text("New version v\(update.version) is ready")
|
|
919
|
-
.font(Typo.monoBold(10))
|
|
920
|
-
.foregroundColor(Palette.detach)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if !update.releaseNotes.isEmpty {
|
|
924
|
-
Text(String(update.releaseNotes.prefix(180)) + (update.releaseNotes.count > 180 ? "..." : ""))
|
|
925
|
-
.font(Typo.caption(9))
|
|
926
|
-
.foregroundColor(Palette.textMuted)
|
|
927
|
-
.lineLimit(3)
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
.padding(8)
|
|
931
|
-
.background(
|
|
932
|
-
RoundedRectangle(cornerRadius: 5)
|
|
933
|
-
.fill(Palette.surfaceHov.opacity(0.65))
|
|
934
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.detach.opacity(0.35), lineWidth: 0.5))
|
|
935
|
-
)
|
|
936
|
-
} else if appUpdater.isChecking {
|
|
937
|
-
Text("Checking for updates...")
|
|
938
|
-
.font(Typo.caption(9))
|
|
939
|
-
.foregroundColor(Palette.textMuted)
|
|
940
|
-
} else if let error = appUpdater.lastError {
|
|
941
|
-
Text(error)
|
|
942
|
-
.font(Typo.caption(9))
|
|
943
|
-
.foregroundColor(Palette.detach.opacity(0.9))
|
|
944
|
-
} else if let checked = appUpdater.lastChecked {
|
|
945
|
-
Text("Last checked \(checked, style: .relative)")
|
|
946
|
-
.font(Typo.caption(9))
|
|
947
|
-
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if let status = appUpdater.statusMessage {
|
|
951
|
-
Text(status)
|
|
952
|
-
.font(Typo.caption(9))
|
|
953
|
-
.foregroundColor(Palette.running.opacity(0.85))
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if let reason = appUpdater.unavailableReason {
|
|
957
|
-
Text(reason)
|
|
958
|
-
.font(Typo.caption(9))
|
|
959
|
-
.foregroundColor(Palette.detach.opacity(0.9))
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
HStack(spacing: 10) {
|
|
963
|
-
Button {
|
|
964
|
-
appUpdater.promptForUpdate()
|
|
965
|
-
} label: {
|
|
966
|
-
Text(appUpdater.isUpdating ? "Preparing..." : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
|
|
967
|
-
.font(Typo.monoBold(10))
|
|
968
|
-
.foregroundColor(Palette.text)
|
|
969
|
-
.padding(.horizontal, 12)
|
|
970
|
-
.padding(.vertical, 5)
|
|
971
|
-
.background(
|
|
972
|
-
RoundedRectangle(cornerRadius: 4)
|
|
973
|
-
.fill(Palette.surfaceHov)
|
|
974
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
975
|
-
)
|
|
976
|
-
}
|
|
977
|
-
.buttonStyle(.plain)
|
|
978
|
-
.disabled(appUpdater.isUpdating)
|
|
979
|
-
|
|
980
|
-
Button {
|
|
981
|
-
Task { await appUpdater.check() }
|
|
982
|
-
} label: {
|
|
983
|
-
Text(appUpdater.isChecking ? "Checking..." : "Check Now")
|
|
984
|
-
.font(Typo.caption(9))
|
|
985
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
986
|
-
}
|
|
987
|
-
.buttonStyle(.plain)
|
|
988
|
-
.disabled(appUpdater.isChecking)
|
|
989
|
-
|
|
990
|
-
Toggle("Auto", isOn: $appUpdater.autoCheckEnabled)
|
|
991
|
-
.font(Typo.caption(9))
|
|
992
|
-
.toggleStyle(.checkbox)
|
|
993
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
994
|
-
|
|
995
|
-
if appUpdater.availableUpdate != nil {
|
|
996
|
-
Button {
|
|
997
|
-
appUpdater.viewCurrentRelease()
|
|
998
|
-
} label: {
|
|
999
|
-
Text("Release Notes")
|
|
1000
|
-
.font(Typo.caption(9))
|
|
1001
|
-
.foregroundColor(Palette.textMuted.opacity(0.9))
|
|
1002
|
-
}
|
|
1003
|
-
.buttonStyle(.plain)
|
|
1004
|
-
|
|
1005
|
-
Button {
|
|
1006
|
-
appUpdater.skipCurrentUpdate()
|
|
1007
|
-
} label: {
|
|
1008
|
-
Text("Skip")
|
|
1009
|
-
.font(Typo.caption(9))
|
|
1010
|
-
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
1011
|
-
}
|
|
1012
|
-
.buttonStyle(.plain)
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
Spacer()
|
|
1016
|
-
|
|
1017
|
-
Text("CLI: `lattices app update`")
|
|
1018
|
-
.font(Typo.caption(9))
|
|
1019
|
-
.foregroundColor(Palette.textMuted.opacity(0.8))
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
.padding(16)
|
|
1025
|
-
.frame(maxWidth: 760, alignment: .leading)
|
|
1026
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
private var behaviorContent: some View {
|
|
1031
|
-
ScrollView {
|
|
1032
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
1033
|
-
settingsCard {
|
|
1034
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1035
|
-
Text("tmux")
|
|
1036
|
-
.font(Typo.mono(11))
|
|
1037
|
-
.foregroundColor(Palette.text)
|
|
1038
|
-
|
|
1039
|
-
HStack {
|
|
1040
|
-
Text("Detach mode")
|
|
1041
|
-
.font(Typo.mono(10))
|
|
1042
|
-
.foregroundColor(Palette.textDim)
|
|
1043
|
-
Spacer()
|
|
1044
|
-
Picker("", selection: $prefs.mode) {
|
|
1045
|
-
Text("Learning").tag(InteractionMode.learning)
|
|
1046
|
-
Text("Auto").tag(InteractionMode.auto)
|
|
1047
|
-
}
|
|
1048
|
-
.pickerStyle(.segmented)
|
|
1049
|
-
.labelsHidden()
|
|
1050
|
-
.frame(width: 160)
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
Text(prefs.mode == .learning
|
|
1054
|
-
? "Shows keybinding hints on detach"
|
|
1055
|
-
: "Detaches sessions silently")
|
|
1056
|
-
.font(Typo.caption(9))
|
|
1057
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
settingsCard {
|
|
1062
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1063
|
-
Text("Window drag snap")
|
|
1064
|
-
.font(Typo.mono(11))
|
|
1065
|
-
.foregroundColor(Palette.text)
|
|
1066
|
-
|
|
1067
|
-
HStack {
|
|
1068
|
-
Text("Drag-to-snap")
|
|
1069
|
-
.font(Typo.mono(10))
|
|
1070
|
-
.foregroundColor(Palette.textDim)
|
|
1071
|
-
Spacer()
|
|
1072
|
-
Toggle("", isOn: $prefs.dragSnapEnabled)
|
|
1073
|
-
.toggleStyle(.switch)
|
|
1074
|
-
.controlSize(.small)
|
|
1075
|
-
.labelsHidden()
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
HStack {
|
|
1079
|
-
Text("Snap modifier")
|
|
1080
|
-
.font(Typo.mono(10))
|
|
1081
|
-
.foregroundColor(Palette.textDim)
|
|
1082
|
-
Spacer()
|
|
1083
|
-
Picker("", selection: snapModifierBinding) {
|
|
1084
|
-
ForEach(SnapModifierKey.allCases) { modifier in
|
|
1085
|
-
Text(modifier.shortLabel).tag(modifier)
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
.pickerStyle(.segmented)
|
|
1089
|
-
.labelsHidden()
|
|
1090
|
-
.frame(width: 220)
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
Text("Dragging stays normal until you hold \(snapModifierBinding.wrappedValue.label). While that key is down, Lattices reveals snap targets and a live preview for the window you’re moving.")
|
|
1094
|
-
.font(Typo.caption(9))
|
|
1095
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1096
|
-
|
|
1097
|
-
cardDivider
|
|
1098
|
-
|
|
1099
|
-
Text("Advanced landing-zone rules still live in ~/.lattices/snap-zones.json. Modifier changes here take effect on the next drag.")
|
|
1100
|
-
.font(Typo.caption(9))
|
|
1101
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
settingsCard {
|
|
1106
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1107
|
-
Text("Mouse gestures")
|
|
1108
|
-
.font(Typo.mono(11))
|
|
1109
|
-
.foregroundColor(Palette.text)
|
|
1110
|
-
|
|
1111
|
-
HStack {
|
|
1112
|
-
Text("Middle-click gestures")
|
|
1113
|
-
.font(Typo.mono(10))
|
|
1114
|
-
.foregroundColor(Palette.textDim)
|
|
1115
|
-
Spacer()
|
|
1116
|
-
Toggle("", isOn: $prefs.mouseGesturesEnabled)
|
|
1117
|
-
.toggleStyle(.switch)
|
|
1118
|
-
.controlSize(.small)
|
|
1119
|
-
.labelsHidden()
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
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.")
|
|
1123
|
-
.font(Typo.caption(9))
|
|
1124
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1125
|
-
|
|
1126
|
-
cardDivider
|
|
1127
|
-
|
|
1128
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
1129
|
-
Text("Active drag mappings")
|
|
1130
|
-
.font(Typo.mono(10))
|
|
1131
|
-
.foregroundColor(Palette.textDim)
|
|
1132
|
-
|
|
1133
|
-
ForEach(mouseShortcutStore.summaryLines.prefix(4), id: \.self) { line in
|
|
1134
|
-
Text(line)
|
|
1135
|
-
.font(Typo.caption(9))
|
|
1136
|
-
.foregroundColor(Palette.textMuted.opacity(0.78))
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
if mouseShortcutStore.summaryLines.isEmpty {
|
|
1140
|
-
Text("No active mappings")
|
|
1141
|
-
.font(Typo.caption(9))
|
|
1142
|
-
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
breakerStatusRow(
|
|
1147
|
-
state: mouseGestureController.breakerState,
|
|
1148
|
-
label: "Mouse gestures"
|
|
1149
|
-
) {
|
|
1150
|
-
mouseGestureController.reArmAfterBreakerTrip()
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
HStack(spacing: 8) {
|
|
1154
|
-
Button {
|
|
1155
|
-
mouseShortcutStore.openConfiguration()
|
|
1156
|
-
} label: {
|
|
1157
|
-
Text("Configure...")
|
|
1158
|
-
.font(Typo.monoBold(10))
|
|
1159
|
-
.foregroundColor(Palette.text)
|
|
1160
|
-
.padding(.horizontal, 12)
|
|
1161
|
-
.padding(.vertical, 4)
|
|
1162
|
-
.background(
|
|
1163
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1164
|
-
.fill(Palette.surfaceHov)
|
|
1165
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1166
|
-
)
|
|
1167
|
-
}
|
|
1168
|
-
.buttonStyle(.plain)
|
|
1169
|
-
|
|
1170
|
-
Button {
|
|
1171
|
-
MouseInputEventViewer.shared.show()
|
|
1172
|
-
} label: {
|
|
1173
|
-
Text("Open Event Viewer")
|
|
1174
|
-
.font(Typo.monoBold(10))
|
|
1175
|
-
.foregroundColor(Palette.text)
|
|
1176
|
-
.padding(.horizontal, 12)
|
|
1177
|
-
.padding(.vertical, 4)
|
|
1178
|
-
.background(
|
|
1179
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1180
|
-
.fill(Palette.surfaceHov)
|
|
1181
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1182
|
-
)
|
|
1183
|
-
}
|
|
1184
|
-
.buttonStyle(.plain)
|
|
1185
|
-
|
|
1186
|
-
Button {
|
|
1187
|
-
mouseShortcutStore.restoreDefaults()
|
|
1188
|
-
} label: {
|
|
1189
|
-
Text("Restore Defaults")
|
|
1190
|
-
.font(Typo.monoBold(10))
|
|
1191
|
-
.foregroundColor(Palette.text)
|
|
1192
|
-
.padding(.horizontal, 12)
|
|
1193
|
-
.padding(.vertical, 4)
|
|
1194
|
-
.background(
|
|
1195
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1196
|
-
.fill(Palette.surfaceHov)
|
|
1197
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1198
|
-
)
|
|
1199
|
-
}
|
|
1200
|
-
.buttonStyle(.plain)
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
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.")
|
|
1204
|
-
.font(Typo.caption(9))
|
|
1205
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
settingsCard {
|
|
1210
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1211
|
-
Text("Keyboard remaps")
|
|
1212
|
-
.font(Typo.mono(11))
|
|
1213
|
-
.foregroundColor(Palette.text)
|
|
1214
|
-
|
|
1215
|
-
HStack {
|
|
1216
|
-
Text("Caps Lock as Hyper")
|
|
1217
|
-
.font(Typo.mono(10))
|
|
1218
|
-
.foregroundColor(Palette.textDim)
|
|
1219
|
-
Spacer()
|
|
1220
|
-
Toggle("", isOn: $prefs.keyboardRemapsEnabled)
|
|
1221
|
-
.toggleStyle(.switch)
|
|
1222
|
-
.controlSize(.small)
|
|
1223
|
-
.labelsHidden()
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
Text("Rules live in ~/.lattices/keyboard-remaps.json. The default maps hold Caps Lock to Hyper and tap Caps Lock to Escape, so the existing Hyper shortcuts work on the laptop keyboard.")
|
|
1227
|
-
.font(Typo.caption(9))
|
|
1228
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1229
|
-
|
|
1230
|
-
cardDivider
|
|
1231
|
-
|
|
1232
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
1233
|
-
Text("Active remaps")
|
|
1234
|
-
.font(Typo.mono(10))
|
|
1235
|
-
.foregroundColor(Palette.textDim)
|
|
1236
|
-
|
|
1237
|
-
ForEach(keyboardRemapStore.summaryLines.prefix(4), id: \.self) { line in
|
|
1238
|
-
Text(line)
|
|
1239
|
-
.font(Typo.caption(9))
|
|
1240
|
-
.foregroundColor(Palette.textMuted.opacity(0.78))
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
if keyboardRemapStore.summaryLines.isEmpty {
|
|
1244
|
-
Text("No active remaps")
|
|
1245
|
-
.font(Typo.caption(9))
|
|
1246
|
-
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
breakerStatusRow(
|
|
1251
|
-
state: keyboardRemapController.breakerState,
|
|
1252
|
-
label: "Keyboard remaps"
|
|
1253
|
-
) {
|
|
1254
|
-
keyboardRemapController.reArmAfterBreakerTrip()
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
HStack(spacing: 8) {
|
|
1258
|
-
Button {
|
|
1259
|
-
keyboardRemapStore.openConfiguration()
|
|
1260
|
-
} label: {
|
|
1261
|
-
Text("Configure...")
|
|
1262
|
-
.font(Typo.monoBold(10))
|
|
1263
|
-
.foregroundColor(Palette.text)
|
|
1264
|
-
.padding(.horizontal, 12)
|
|
1265
|
-
.padding(.vertical, 4)
|
|
1266
|
-
.background(
|
|
1267
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1268
|
-
.fill(Palette.surfaceHov)
|
|
1269
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1270
|
-
)
|
|
1271
|
-
}
|
|
1272
|
-
.buttonStyle(.plain)
|
|
1273
|
-
|
|
1274
|
-
Button {
|
|
1275
|
-
keyboardRemapStore.restoreDefaults()
|
|
1276
|
-
} label: {
|
|
1277
|
-
Text("Restore Defaults")
|
|
1278
|
-
.font(Typo.monoBold(10))
|
|
1279
|
-
.foregroundColor(Palette.text)
|
|
1280
|
-
.padding(.horizontal, 12)
|
|
1281
|
-
.padding(.vertical, 4)
|
|
1282
|
-
.background(
|
|
1283
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1284
|
-
.fill(Palette.surfaceHov)
|
|
1285
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1286
|
-
)
|
|
1287
|
-
}
|
|
1288
|
-
.buttonStyle(.plain)
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
.padding(16)
|
|
1294
|
-
.frame(maxWidth: 760, alignment: .leading)
|
|
1295
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// MARK: - Companion
|
|
1300
|
-
|
|
1301
|
-
private var companionContent: some View {
|
|
1302
|
-
ScrollView {
|
|
1303
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
1304
|
-
companionBridgeOverviewCard
|
|
1305
|
-
companionTrustedDevicesCard
|
|
1306
|
-
companionCockpitCard
|
|
1307
|
-
}
|
|
1308
|
-
.padding(16)
|
|
1309
|
-
.frame(maxWidth: 760, alignment: .leading)
|
|
1310
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
private var companionBridgeOverviewCard: some View {
|
|
1315
|
-
settingsCard {
|
|
1316
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
1317
|
-
HStack(alignment: .top, spacing: 10) {
|
|
1318
|
-
RoundedRectangle(cornerRadius: 6)
|
|
1319
|
-
.fill(Palette.running.opacity(0.14))
|
|
1320
|
-
.overlay(
|
|
1321
|
-
Image(systemName: "lock.shield")
|
|
1322
|
-
.font(.system(size: 13, weight: .semibold))
|
|
1323
|
-
.foregroundColor(Palette.running)
|
|
1324
|
-
)
|
|
1325
|
-
.frame(width: 30, height: 30)
|
|
1326
|
-
|
|
1327
|
-
VStack(alignment: .leading, spacing: 3) {
|
|
1328
|
-
Text(prefs.companionBridgeEnabled ? "Secure local bridge" : "Local bridge off")
|
|
1329
|
-
.font(Typo.mono(12))
|
|
1330
|
-
.foregroundColor(Palette.text)
|
|
1331
|
-
Text(prefs.companionBridgeEnabled
|
|
1332
|
-
? "Bonjour discovery with explicit Mac approval, signed requests, encrypted payloads, and capability grants."
|
|
1333
|
-
: "The companion bridge is not listening or advertising on the local network until you turn it on.")
|
|
1334
|
-
.font(Typo.caption(10))
|
|
1335
|
-
.foregroundColor(Palette.textMuted)
|
|
1336
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
Spacer()
|
|
1340
|
-
|
|
1341
|
-
Toggle("", isOn: $prefs.companionBridgeEnabled)
|
|
1342
|
-
.toggleStyle(.switch)
|
|
1343
|
-
.controlSize(.small)
|
|
1344
|
-
.labelsHidden()
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
cardDivider
|
|
1348
|
-
|
|
1349
|
-
LazyVGrid(
|
|
1350
|
-
columns: [
|
|
1351
|
-
GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
|
|
1352
|
-
GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
|
|
1353
|
-
GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
|
|
1354
|
-
],
|
|
1355
|
-
alignment: .leading,
|
|
1356
|
-
spacing: 10
|
|
1357
|
-
) {
|
|
1358
|
-
companionBridgeFact(
|
|
1359
|
-
label: "Status",
|
|
1360
|
-
value: prefs.companionBridgeEnabled ? "enabled" : "off"
|
|
1361
|
-
)
|
|
1362
|
-
companionBridgeFact(
|
|
1363
|
-
label: "Port",
|
|
1364
|
-
value: String(LatticesCompanionBridgeServer.defaultPort)
|
|
1365
|
-
)
|
|
1366
|
-
companionBridgeFact(
|
|
1367
|
-
label: "Protocol",
|
|
1368
|
-
value: "v\(LatticesCompanionBridgeServer.protocolVersion)"
|
|
1369
|
-
)
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
1373
|
-
Text("Enable deep link")
|
|
1374
|
-
.font(Typo.mono(10))
|
|
1375
|
-
.foregroundColor(Palette.textDim)
|
|
1376
|
-
Text("lattices://companion/enable")
|
|
1377
|
-
.font(Typo.monoBold(12))
|
|
1378
|
-
.foregroundColor(Palette.text)
|
|
1379
|
-
.textSelection(.enabled)
|
|
1380
|
-
}
|
|
1381
|
-
.padding(10)
|
|
1382
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1383
|
-
.background(shortcutsInsetPanel)
|
|
1384
|
-
|
|
1385
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
1386
|
-
Text("Mac bridge fingerprint")
|
|
1387
|
-
.font(Typo.mono(10))
|
|
1388
|
-
.foregroundColor(Palette.textDim)
|
|
1389
|
-
Text(LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint)
|
|
1390
|
-
.font(Typo.monoBold(13))
|
|
1391
|
-
.foregroundColor(Palette.text)
|
|
1392
|
-
.textSelection(.enabled)
|
|
1393
|
-
}
|
|
1394
|
-
.padding(10)
|
|
1395
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1396
|
-
.background(shortcutsInsetPanel)
|
|
1397
|
-
|
|
1398
|
-
HStack(spacing: 6) {
|
|
1399
|
-
ForEach(DeckBridgeCapability.defaultCompanionCapabilities, id: \.self) { capability in
|
|
1400
|
-
companionCapabilityBadge(capability)
|
|
1401
|
-
}
|
|
1402
|
-
Spacer(minLength: 0)
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
private var companionTrustedDevicesCard: some View {
|
|
1409
|
-
let trustedDevices = companionTrustedDevices(revision: companionTrustRevision)
|
|
1410
|
-
|
|
1411
|
-
return settingsCard {
|
|
1412
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
1413
|
-
HStack(alignment: .top, spacing: 12) {
|
|
1414
|
-
VStack(alignment: .leading, spacing: 4) {
|
|
1415
|
-
Text("Paired devices")
|
|
1416
|
-
.font(Typo.mono(12))
|
|
1417
|
-
.foregroundColor(Palette.text)
|
|
1418
|
-
Text("Only trusted devices can call protected deck and input routes. Pairing grants are listed per device.")
|
|
1419
|
-
.font(Typo.caption(10))
|
|
1420
|
-
.foregroundColor(Palette.textMuted)
|
|
1421
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
Spacer()
|
|
1425
|
-
|
|
1426
|
-
HStack(spacing: 8) {
|
|
1427
|
-
Button {
|
|
1428
|
-
companionTrustRevision += 1
|
|
1429
|
-
} label: {
|
|
1430
|
-
Image(systemName: "arrow.clockwise")
|
|
1431
|
-
.font(.system(size: 10, weight: .semibold))
|
|
1432
|
-
.foregroundColor(Palette.textDim)
|
|
1433
|
-
.frame(width: 24, height: 24)
|
|
1434
|
-
.background(
|
|
1435
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1436
|
-
.fill(Palette.surfaceHov)
|
|
1437
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1438
|
-
)
|
|
1439
|
-
}
|
|
1440
|
-
.buttonStyle(.plain)
|
|
1441
|
-
|
|
1442
|
-
if trustedDevices.isEmpty == false {
|
|
1443
|
-
Button {
|
|
1444
|
-
guard confirmForgetTrustedDevices() else { return }
|
|
1445
|
-
LatticesCompanionSecurityCoordinator.shared.clearTrustedDevices()
|
|
1446
|
-
companionTrustRevision += 1
|
|
1447
|
-
} label: {
|
|
1448
|
-
Text("Forget All")
|
|
1449
|
-
.font(Typo.monoBold(10))
|
|
1450
|
-
.foregroundColor(Palette.kill.opacity(0.9))
|
|
1451
|
-
.padding(.horizontal, 10)
|
|
1452
|
-
.padding(.vertical, 5)
|
|
1453
|
-
.background(
|
|
1454
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1455
|
-
.fill(Palette.kill.opacity(0.10))
|
|
1456
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5))
|
|
1457
|
-
)
|
|
1458
|
-
}
|
|
1459
|
-
.buttonStyle(.plain)
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
if trustedDevices.isEmpty {
|
|
1465
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
1466
|
-
Image(systemName: "ipad.and.iphone")
|
|
1467
|
-
.font(.system(size: 18, weight: .semibold))
|
|
1468
|
-
.foregroundColor(Palette.textMuted)
|
|
1469
|
-
|
|
1470
|
-
Text("No paired iPad or iPhone devices yet.")
|
|
1471
|
-
.font(Typo.caption(10.5))
|
|
1472
|
-
.foregroundColor(Palette.textMuted)
|
|
1473
|
-
|
|
1474
|
-
Text("Open the Lattices companion app on your iPad and select this Mac. You’ll approve the pairing prompt here.")
|
|
1475
|
-
.font(Typo.caption(9.5))
|
|
1476
|
-
.foregroundColor(Palette.textMuted.opacity(0.72))
|
|
1477
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1478
|
-
}
|
|
1479
|
-
.padding(12)
|
|
1480
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1481
|
-
.background(shortcutsInsetPanel)
|
|
1482
|
-
} else {
|
|
1483
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
1484
|
-
ForEach(trustedDevices) { device in
|
|
1485
|
-
companionDeviceRow(device)
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
private func companionBridgeFact(label: String, value: String) -> some View {
|
|
1494
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
1495
|
-
Text(label.uppercased())
|
|
1496
|
-
.font(Typo.pixel(11))
|
|
1497
|
-
.foregroundColor(Palette.textDim)
|
|
1498
|
-
.tracking(1)
|
|
1499
|
-
Text(value)
|
|
1500
|
-
.font(Typo.monoBold(11))
|
|
1501
|
-
.foregroundColor(Palette.text)
|
|
1502
|
-
.lineLimit(1)
|
|
1503
|
-
.truncationMode(.middle)
|
|
1504
|
-
}
|
|
1505
|
-
.padding(10)
|
|
1506
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1507
|
-
.background(shortcutsInsetPanel)
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
private func companionDeviceRow(_ device: DeckTrustedDeviceSummary) -> some View {
|
|
1511
|
-
HStack(alignment: .top, spacing: 10) {
|
|
1512
|
-
RoundedRectangle(cornerRadius: 6)
|
|
1513
|
-
.fill(Palette.surfaceHov)
|
|
1514
|
-
.overlay(
|
|
1515
|
-
Image(systemName: companionDeviceIcon(for: device.name))
|
|
1516
|
-
.font(.system(size: 13, weight: .semibold))
|
|
1517
|
-
.foregroundColor(Palette.textDim)
|
|
1518
|
-
)
|
|
1519
|
-
.frame(width: 30, height: 30)
|
|
1520
|
-
|
|
1521
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
1522
|
-
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
1523
|
-
Text(device.name)
|
|
1524
|
-
.font(Typo.monoBold(11))
|
|
1525
|
-
.foregroundColor(Palette.text)
|
|
1526
|
-
.lineLimit(1)
|
|
1527
|
-
|
|
1528
|
-
Text(device.fingerprint)
|
|
1529
|
-
.font(Typo.mono(10))
|
|
1530
|
-
.foregroundColor(Palette.textMuted)
|
|
1531
|
-
.lineLimit(1)
|
|
1532
|
-
|
|
1533
|
-
Spacer(minLength: 0)
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
HStack(spacing: 10) {
|
|
1537
|
-
Text("Paired \(relativeTimestamp(device.pairedAt))")
|
|
1538
|
-
Text("Last seen \(relativeTimestamp(device.lastSeenAt))")
|
|
1539
|
-
}
|
|
1540
|
-
.font(Typo.caption(9.5))
|
|
1541
|
-
.foregroundColor(Palette.textMuted.opacity(0.78))
|
|
1542
|
-
|
|
1543
|
-
HStack(spacing: 6) {
|
|
1544
|
-
ForEach(device.capabilities, id: \.self) { capability in
|
|
1545
|
-
companionCapabilityBadge(capability)
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
Spacer(minLength: 0)
|
|
1551
|
-
|
|
1552
|
-
Button {
|
|
1553
|
-
guard confirmRevokeTrustedDevice(device) else { return }
|
|
1554
|
-
LatticesCompanionSecurityCoordinator.shared.revokeTrustedDevice(id: device.id)
|
|
1555
|
-
companionTrustRevision += 1
|
|
1556
|
-
} label: {
|
|
1557
|
-
HStack(spacing: 5) {
|
|
1558
|
-
Image(systemName: "xmark.shield")
|
|
1559
|
-
.font(.system(size: 10, weight: .semibold))
|
|
1560
|
-
Text("Revoke")
|
|
1561
|
-
.font(Typo.monoBold(9.5))
|
|
1562
|
-
}
|
|
1563
|
-
.foregroundColor(Palette.kill.opacity(0.95))
|
|
1564
|
-
.padding(.horizontal, 8)
|
|
1565
|
-
.padding(.vertical, 5)
|
|
1566
|
-
.background(
|
|
1567
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1568
|
-
.fill(Palette.kill.opacity(0.10))
|
|
1569
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5))
|
|
1570
|
-
)
|
|
1571
|
-
}
|
|
1572
|
-
.buttonStyle(.plain)
|
|
1573
|
-
.help("Revoke this paired device")
|
|
1574
|
-
}
|
|
1575
|
-
.padding(12)
|
|
1576
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1577
|
-
.background(shortcutsInsetPanel)
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
private func companionCapabilityBadge(_ capability: String) -> some View {
|
|
1581
|
-
Text(companionCapabilityLabel(capability))
|
|
1582
|
-
.font(Typo.monoBold(9))
|
|
1583
|
-
.foregroundColor(Palette.running.opacity(0.92))
|
|
1584
|
-
.padding(.horizontal, 7)
|
|
1585
|
-
.padding(.vertical, 3)
|
|
1586
|
-
.background(
|
|
1587
|
-
Capsule()
|
|
1588
|
-
.fill(Palette.running.opacity(0.10))
|
|
1589
|
-
.overlay(Capsule().strokeBorder(Palette.running.opacity(0.18), lineWidth: 0.5))
|
|
1590
|
-
)
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
private func companionCapabilityLabel(_ capability: String) -> String {
|
|
1594
|
-
switch capability {
|
|
1595
|
-
case DeckBridgeCapability.deckRead:
|
|
1596
|
-
return "Deck Read"
|
|
1597
|
-
case DeckBridgeCapability.deckPerform:
|
|
1598
|
-
return "Deck Actions"
|
|
1599
|
-
case DeckBridgeCapability.inputTrackpad:
|
|
1600
|
-
return "Trackpad"
|
|
1601
|
-
default:
|
|
1602
|
-
return capability
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
private func companionDeviceIcon(for name: String) -> String {
|
|
1607
|
-
name.localizedCaseInsensitiveContains("ipad") ? "ipad" : "iphone"
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
private func confirmForgetTrustedDevices() -> Bool {
|
|
1611
|
-
let alert = NSAlert()
|
|
1612
|
-
alert.alertStyle = .warning
|
|
1613
|
-
alert.messageText = "Forget all paired companion devices?"
|
|
1614
|
-
alert.informativeText = "Your iPad or iPhone will need to pair again before it can control Lattices."
|
|
1615
|
-
alert.addButton(withTitle: "Forget Devices")
|
|
1616
|
-
alert.addButton(withTitle: "Cancel")
|
|
1617
|
-
return alert.runModal() == .alertFirstButtonReturn
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
private func confirmRevokeTrustedDevice(_ device: DeckTrustedDeviceSummary) -> Bool {
|
|
1621
|
-
let alert = NSAlert()
|
|
1622
|
-
alert.alertStyle = .warning
|
|
1623
|
-
alert.messageText = "Revoke \(device.name)?"
|
|
1624
|
-
alert.informativeText = """
|
|
1625
|
-
This removes the paired-device trust record for \(device.name).
|
|
1626
|
-
|
|
1627
|
-
Fingerprint: \(device.fingerprint)
|
|
1628
|
-
|
|
1629
|
-
The device will need to pair again before it can control Lattices.
|
|
1630
|
-
"""
|
|
1631
|
-
alert.addButton(withTitle: "Revoke Device")
|
|
1632
|
-
alert.addButton(withTitle: "Cancel")
|
|
1633
|
-
return alert.runModal() == .alertFirstButtonReturn
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// MARK: - AI
|
|
1637
|
-
|
|
1638
|
-
private var aiContent: some View {
|
|
1639
|
-
ScrollView {
|
|
1640
|
-
VStack(spacing: 12) {
|
|
1641
|
-
// ── Claude CLI ──
|
|
1642
|
-
settingsCard {
|
|
1643
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1644
|
-
HStack(spacing: 8) {
|
|
1645
|
-
Image(systemName: "sparkles")
|
|
1646
|
-
.font(.system(size: 11, weight: .medium))
|
|
1647
|
-
.foregroundColor(Palette.running)
|
|
1648
|
-
Text("Claude CLI")
|
|
1649
|
-
.font(Typo.mono(12))
|
|
1650
|
-
.foregroundColor(Palette.text)
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
HStack(spacing: 6) {
|
|
1654
|
-
TextField("Auto-detected", text: $prefs.claudePath)
|
|
1655
|
-
.textFieldStyle(.plain)
|
|
1656
|
-
.font(Typo.mono(11))
|
|
1657
|
-
.foregroundColor(Palette.text)
|
|
1658
|
-
.padding(.horizontal, 8)
|
|
1659
|
-
.padding(.vertical, 5)
|
|
1660
|
-
.background(
|
|
1661
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1662
|
-
.fill(Color.white.opacity(0.06))
|
|
1663
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
|
|
1664
|
-
)
|
|
1665
|
-
|
|
1666
|
-
Button {
|
|
1667
|
-
if let resolved = Preferences.resolveClaudePath() {
|
|
1668
|
-
prefs.claudePath = resolved
|
|
1669
|
-
}
|
|
1670
|
-
} label: {
|
|
1671
|
-
Text("Detect")
|
|
1672
|
-
.font(Typo.monoBold(10))
|
|
1673
|
-
.foregroundColor(Palette.text)
|
|
1674
|
-
.padding(.horizontal, 10)
|
|
1675
|
-
.padding(.vertical, 4)
|
|
1676
|
-
.background(
|
|
1677
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1678
|
-
.fill(Palette.surfaceHov)
|
|
1679
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1680
|
-
)
|
|
1681
|
-
}
|
|
1682
|
-
.buttonStyle(.plain)
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
let resolved = Preferences.resolveClaudePath()
|
|
1686
|
-
if let path = resolved {
|
|
1687
|
-
Text("Found: \(path)")
|
|
1688
|
-
.font(Typo.caption(9))
|
|
1689
|
-
.foregroundColor(Palette.running.opacity(0.8))
|
|
1690
|
-
} else {
|
|
1691
|
-
Text("Not found — install with: npm i -g @anthropic-ai/claude-code")
|
|
1692
|
-
.font(Typo.caption(9))
|
|
1693
|
-
.foregroundColor(Palette.detach)
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// ── Advisor ──
|
|
1699
|
-
settingsCard {
|
|
1700
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1701
|
-
Text("Voice advisor")
|
|
1702
|
-
.font(Typo.mono(11))
|
|
1703
|
-
.foregroundColor(Palette.text)
|
|
1704
|
-
|
|
1705
|
-
HStack {
|
|
1706
|
-
Text("Model")
|
|
1707
|
-
.font(Typo.mono(10))
|
|
1708
|
-
.foregroundColor(Palette.textDim)
|
|
1709
|
-
Spacer()
|
|
1710
|
-
Picker("", selection: $prefs.advisorModel) {
|
|
1711
|
-
Text("Haiku").tag("haiku")
|
|
1712
|
-
Text("Sonnet").tag("sonnet")
|
|
1713
|
-
}
|
|
1714
|
-
.pickerStyle(.segmented)
|
|
1715
|
-
.labelsHidden()
|
|
1716
|
-
.frame(width: 160)
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
Text("Haiku is fast and cheap. Sonnet is smarter but slower.")
|
|
1720
|
-
.font(Typo.caption(9))
|
|
1721
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1722
|
-
|
|
1723
|
-
cardDivider
|
|
1724
|
-
|
|
1725
|
-
HStack {
|
|
1726
|
-
Text("Budget per session")
|
|
1727
|
-
.font(Typo.mono(10))
|
|
1728
|
-
.foregroundColor(Palette.textDim)
|
|
1729
|
-
Spacer()
|
|
1730
|
-
HStack(spacing: 4) {
|
|
1731
|
-
Text("$")
|
|
1732
|
-
.font(Typo.mono(11))
|
|
1733
|
-
.foregroundColor(Palette.textDim)
|
|
1734
|
-
TextField("0.50", value: $prefs.advisorBudgetUSD, formatter: {
|
|
1735
|
-
let f = NumberFormatter()
|
|
1736
|
-
f.numberStyle = .decimal
|
|
1737
|
-
f.minimumFractionDigits = 2
|
|
1738
|
-
f.maximumFractionDigits = 2
|
|
1739
|
-
return f
|
|
1740
|
-
}())
|
|
1741
|
-
.textFieldStyle(.plain)
|
|
1742
|
-
.font(Typo.monoBold(11))
|
|
1743
|
-
.foregroundColor(Palette.text)
|
|
1744
|
-
.multilineTextAlignment(.center)
|
|
1745
|
-
.frame(width: 50)
|
|
1746
|
-
.padding(.horizontal, 4)
|
|
1747
|
-
.padding(.vertical, 3)
|
|
1748
|
-
.background(
|
|
1749
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1750
|
-
.fill(Color.white.opacity(0.06))
|
|
1751
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
|
|
1752
|
-
)
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
Text("Max spend per Claude CLI invocation")
|
|
1757
|
-
.font(Typo.caption(9))
|
|
1758
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
1759
|
-
|
|
1760
|
-
cardDivider
|
|
1761
|
-
|
|
1762
|
-
// Session stats
|
|
1763
|
-
let stats = AgentPool.shared.haiku.sessionStats
|
|
1764
|
-
HStack(spacing: 12) {
|
|
1765
|
-
if stats.contextWindow > 0 {
|
|
1766
|
-
HStack(spacing: 4) {
|
|
1767
|
-
Circle()
|
|
1768
|
-
.fill(stats.contextUsage > 0.6 ? Palette.detach : Palette.running)
|
|
1769
|
-
.frame(width: 5, height: 5)
|
|
1770
|
-
Text("Context: \(Int(stats.contextUsage * 100))%")
|
|
1771
|
-
.font(Typo.mono(10))
|
|
1772
|
-
.foregroundColor(Palette.textMuted)
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
if stats.costUSD > 0 {
|
|
1776
|
-
Text("Session cost: $\(String(format: "%.3f", stats.costUSD))")
|
|
1777
|
-
.font(Typo.mono(10))
|
|
1778
|
-
.foregroundColor(Palette.textMuted)
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
Spacer()
|
|
1782
|
-
|
|
1783
|
-
let learningCount = AdvisorLearningStore.shared.entryCount
|
|
1784
|
-
if learningCount > 0 {
|
|
1785
|
-
Text("\(learningCount) learned")
|
|
1786
|
-
.font(Typo.mono(9))
|
|
1787
|
-
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
.padding(16)
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
private var buildChannelBadge: some View {
|
|
1798
|
-
let tint = LatticesRuntime.isDevBuild ? Palette.detach : Palette.running
|
|
1799
|
-
|
|
1800
|
-
return Text(LatticesRuntime.buildChannelLabel)
|
|
1801
|
-
.font(Typo.monoBold(9))
|
|
1802
|
-
.foregroundColor(tint)
|
|
1803
|
-
.padding(.horizontal, 6)
|
|
1804
|
-
.padding(.vertical, 3)
|
|
1805
|
-
.background(
|
|
1806
|
-
Capsule()
|
|
1807
|
-
.fill(tint.opacity(0.12))
|
|
1808
|
-
)
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
// MARK: - Search & OCR
|
|
1812
|
-
|
|
1813
|
-
private func ocrNumField(_ value: Binding<Double>, width: CGFloat = 50) -> some View {
|
|
1814
|
-
TextField("", value: value, formatter: NumberFormatter())
|
|
1815
|
-
.textFieldStyle(.plain)
|
|
1816
|
-
.font(Typo.monoBold(11))
|
|
1817
|
-
.foregroundColor(Palette.text)
|
|
1818
|
-
.multilineTextAlignment(.center)
|
|
1819
|
-
.frame(width: width)
|
|
1820
|
-
.padding(.horizontal, 4)
|
|
1821
|
-
.padding(.vertical, 3)
|
|
1822
|
-
.background(
|
|
1823
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1824
|
-
.fill(Color.white.opacity(0.06))
|
|
1825
|
-
.overlay(
|
|
1826
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1827
|
-
.strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
|
|
1828
|
-
)
|
|
1829
|
-
)
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
private func ocrIntField(_ value: Binding<Int>, width: CGFloat = 36) -> some View {
|
|
1833
|
-
TextField("", value: value, formatter: NumberFormatter())
|
|
1834
|
-
.textFieldStyle(.plain)
|
|
1835
|
-
.font(Typo.monoBold(11))
|
|
1836
|
-
.foregroundColor(Palette.text)
|
|
1837
|
-
.multilineTextAlignment(.center)
|
|
1838
|
-
.frame(width: width)
|
|
1839
|
-
.padding(.horizontal, 4)
|
|
1840
|
-
.padding(.vertical, 3)
|
|
1841
|
-
.background(
|
|
1842
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1843
|
-
.fill(Color.white.opacity(0.06))
|
|
1844
|
-
.overlay(
|
|
1845
|
-
RoundedRectangle(cornerRadius: 5)
|
|
1846
|
-
.strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
|
|
1847
|
-
)
|
|
1848
|
-
)
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
private func ocrSectionLabel(_ text: String) -> some View {
|
|
1852
|
-
Text(text)
|
|
1853
|
-
.font(Typo.monoBold(10))
|
|
1854
|
-
.foregroundColor(Palette.textDim)
|
|
1855
|
-
.tracking(0.5)
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
private var searchOcrContent: some View {
|
|
1859
|
-
ScrollView {
|
|
1860
|
-
VStack(spacing: 12) {
|
|
1861
|
-
// ── Screen Text Recognition Card ──
|
|
1862
|
-
settingsCard {
|
|
1863
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1864
|
-
// Header row: label + toggle
|
|
1865
|
-
HStack {
|
|
1866
|
-
HStack(spacing: 8) {
|
|
1867
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1868
|
-
.fill(prefs.ocrEnabled ? Palette.running.opacity(0.15) : Palette.surface)
|
|
1869
|
-
.overlay(
|
|
1870
|
-
Image(systemName: "text.viewfinder")
|
|
1871
|
-
.font(.system(size: 11, weight: .medium))
|
|
1872
|
-
.foregroundColor(prefs.ocrEnabled ? Palette.running : Palette.textMuted)
|
|
1873
|
-
)
|
|
1874
|
-
.frame(width: 24, height: 24)
|
|
1875
|
-
|
|
1876
|
-
VStack(alignment: .leading, spacing: 1) {
|
|
1877
|
-
Text("Screen text recognition")
|
|
1878
|
-
.font(Typo.mono(12))
|
|
1879
|
-
.foregroundColor(Palette.text)
|
|
1880
|
-
Text("Vision OCR on visible windows")
|
|
1881
|
-
.font(Typo.caption(10))
|
|
1882
|
-
.foregroundColor(Palette.textMuted)
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
Spacer()
|
|
1886
|
-
Toggle("", isOn: Binding(
|
|
1887
|
-
get: { prefs.ocrEnabled },
|
|
1888
|
-
set: { OcrModel.shared.setEnabled($0) }
|
|
1889
|
-
))
|
|
1890
|
-
.toggleStyle(.switch)
|
|
1891
|
-
.controlSize(.small)
|
|
1892
|
-
.labelsHidden()
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
// Accuracy
|
|
1896
|
-
HStack(spacing: 8) {
|
|
1897
|
-
Text("Accuracy")
|
|
1898
|
-
.font(Typo.mono(10))
|
|
1899
|
-
.foregroundColor(Palette.textDim)
|
|
1900
|
-
Picker("", selection: $prefs.ocrAccuracy) {
|
|
1901
|
-
Text("Accurate").tag("accurate")
|
|
1902
|
-
Text("Fast").tag("fast")
|
|
1903
|
-
}
|
|
1904
|
-
.pickerStyle(.segmented)
|
|
1905
|
-
.labelsHidden()
|
|
1906
|
-
.frame(width: 140)
|
|
1907
|
-
Spacer()
|
|
1908
|
-
}
|
|
1909
|
-
.padding(.leading, 32)
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
// ── Scan Schedule Card ──
|
|
1914
|
-
settingsCard {
|
|
1915
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
1916
|
-
ocrSectionLabel("Schedule")
|
|
1917
|
-
|
|
1918
|
-
// Quick scan sentence
|
|
1919
|
-
HStack(spacing: 0) {
|
|
1920
|
-
Text("Quick scan top ")
|
|
1921
|
-
.font(Typo.mono(11))
|
|
1922
|
-
.foregroundColor(Palette.textDim)
|
|
1923
|
-
ocrIntField($prefs.ocrQuickLimit, width: 32)
|
|
1924
|
-
Text(" windows every ")
|
|
1925
|
-
.font(Typo.mono(11))
|
|
1926
|
-
.foregroundColor(Palette.textDim)
|
|
1927
|
-
ocrNumField($prefs.ocrQuickInterval, width: 42)
|
|
1928
|
-
Text("s")
|
|
1929
|
-
.font(Typo.mono(11))
|
|
1930
|
-
.foregroundColor(Palette.textDim)
|
|
1931
|
-
Spacer()
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
cardDivider
|
|
1935
|
-
|
|
1936
|
-
// Deep scan sentence
|
|
1937
|
-
HStack(spacing: 0) {
|
|
1938
|
-
Text("Deep scan up to ")
|
|
1939
|
-
.font(Typo.mono(11))
|
|
1940
|
-
.foregroundColor(Palette.textDim)
|
|
1941
|
-
ocrIntField($prefs.ocrDeepLimit, width: 32)
|
|
1942
|
-
Text(" windows every ")
|
|
1943
|
-
.font(Typo.mono(11))
|
|
1944
|
-
.foregroundColor(Palette.textDim)
|
|
1945
|
-
ocrNumField($prefs.ocrDeepInterval, width: 52)
|
|
1946
|
-
Text("s")
|
|
1947
|
-
.font(Typo.mono(11))
|
|
1948
|
-
.foregroundColor(Palette.textDim)
|
|
1949
|
-
Spacer()
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
HStack(spacing: 0) {
|
|
1953
|
-
Text("OCR budget: ")
|
|
1954
|
-
.font(Typo.mono(11))
|
|
1955
|
-
.foregroundColor(Palette.textDim)
|
|
1956
|
-
ocrIntField($prefs.ocrDeepBudget, width: 32)
|
|
1957
|
-
Text(" windows per scan")
|
|
1958
|
-
.font(Typo.mono(11))
|
|
1959
|
-
.foregroundColor(Palette.textDim)
|
|
1960
|
-
Spacer()
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
// Human-readable deep interval
|
|
1964
|
-
let h = Int(prefs.ocrDeepInterval / 3600)
|
|
1965
|
-
let m = Int(prefs.ocrDeepInterval.truncatingRemainder(dividingBy: 3600) / 60)
|
|
1966
|
-
if h > 0 || m > 0 {
|
|
1967
|
-
Text("≈ \(h > 0 ? "\(h)h" : "")\(m > 0 ? " \(m)m" : "")")
|
|
1968
|
-
.font(Typo.caption(9))
|
|
1969
|
-
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
1970
|
-
.padding(.leading, 2)
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
// ── Status Card ──
|
|
1976
|
-
settingsCard {
|
|
1977
|
-
HStack(spacing: 8) {
|
|
1978
|
-
let ocrResults = OcrModel.shared.results
|
|
1979
|
-
let isScanning = OcrModel.shared.isScanning
|
|
1980
|
-
|
|
1981
|
-
Circle()
|
|
1982
|
-
.fill(isScanning ? Palette.detach : (prefs.ocrEnabled ? Palette.running : Palette.textMuted))
|
|
1983
|
-
.frame(width: 6, height: 6)
|
|
1984
|
-
|
|
1985
|
-
Text(isScanning ? "Scanning..." : (prefs.ocrEnabled ? "\(ocrResults.count) windows cached" : "Disabled"))
|
|
1986
|
-
.font(Typo.mono(10))
|
|
1987
|
-
.foregroundColor(Palette.textMuted)
|
|
1988
|
-
|
|
1989
|
-
Spacer()
|
|
1990
|
-
|
|
1991
|
-
Button {
|
|
1992
|
-
OcrModel.shared.scan()
|
|
1993
|
-
} label: {
|
|
1994
|
-
HStack(spacing: 4) {
|
|
1995
|
-
Image(systemName: "arrow.clockwise")
|
|
1996
|
-
.font(.system(size: 9, weight: .semibold))
|
|
1997
|
-
Text("Scan Now")
|
|
1998
|
-
.font(Typo.monoBold(10))
|
|
1999
|
-
}
|
|
2000
|
-
.foregroundColor(prefs.ocrEnabled ? Palette.text : Palette.textMuted)
|
|
2001
|
-
.padding(.horizontal, 10)
|
|
2002
|
-
.padding(.vertical, 4)
|
|
2003
|
-
.background(
|
|
2004
|
-
RoundedRectangle(cornerRadius: 4)
|
|
2005
|
-
.fill(prefs.ocrEnabled ? Palette.surfaceHov : Palette.surface)
|
|
2006
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
2007
|
-
)
|
|
2008
|
-
}
|
|
2009
|
-
.buttonStyle(.plain)
|
|
2010
|
-
.disabled(!prefs.ocrEnabled)
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
// ── Recent Captures ──
|
|
2015
|
-
recentCapturesSection
|
|
2016
|
-
}
|
|
2017
|
-
.padding(16)
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
// MARK: - Recent Captures Browser
|
|
2022
|
-
|
|
2023
|
-
private var recentCapturesSection: some View {
|
|
2024
|
-
let ocrResults = OcrModel.shared.results
|
|
2025
|
-
let grouped = Dictionary(grouping: ocrResults.values, by: \.app)
|
|
2026
|
-
.sorted { $0.value.count > $1.value.count }
|
|
2027
|
-
|
|
2028
|
-
return Group {
|
|
2029
|
-
if !grouped.isEmpty {
|
|
2030
|
-
settingsCard {
|
|
2031
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2032
|
-
ocrSectionLabel("Recent Captures")
|
|
2033
|
-
|
|
2034
|
-
ForEach(grouped, id: \.key) { app, windows in
|
|
2035
|
-
ocrAppGroup(app: app, windows: windows.sorted { $0.timestamp > $1.timestamp })
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
private func ocrAppGroup(app: String, windows: [OcrWindowResult]) -> some View {
|
|
2044
|
-
let isCollapsed = collapsedOcrApps.contains(app)
|
|
2045
|
-
|
|
2046
|
-
return VStack(alignment: .leading, spacing: 0) {
|
|
2047
|
-
// App header
|
|
2048
|
-
Button {
|
|
2049
|
-
withAnimation(.easeInOut(duration: 0.15)) {
|
|
2050
|
-
if isCollapsed {
|
|
2051
|
-
collapsedOcrApps.remove(app)
|
|
2052
|
-
} else {
|
|
2053
|
-
collapsedOcrApps.insert(app)
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
} label: {
|
|
2057
|
-
HStack(spacing: 6) {
|
|
2058
|
-
Image(systemName: isCollapsed ? "chevron.right" : "chevron.down")
|
|
2059
|
-
.font(.system(size: 8, weight: .semibold))
|
|
2060
|
-
.foregroundColor(Palette.textMuted)
|
|
2061
|
-
.frame(width: 10)
|
|
2062
|
-
|
|
2063
|
-
Text(app)
|
|
2064
|
-
.font(Typo.monoBold(11))
|
|
2065
|
-
.foregroundColor(Palette.text)
|
|
2066
|
-
|
|
2067
|
-
Text("(\(windows.count))")
|
|
2068
|
-
.font(Typo.mono(10))
|
|
2069
|
-
.foregroundColor(Palette.textMuted)
|
|
2070
|
-
|
|
2071
|
-
Spacer()
|
|
2072
|
-
}
|
|
2073
|
-
.padding(.vertical, 4)
|
|
2074
|
-
.contentShape(Rectangle())
|
|
2075
|
-
}
|
|
2076
|
-
.buttonStyle(.plain)
|
|
2077
|
-
|
|
2078
|
-
if !isCollapsed {
|
|
2079
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
2080
|
-
ForEach(windows, id: \.wid) { win in
|
|
2081
|
-
ocrWindowRow(win)
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
.padding(.leading, 16)
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
private func ocrWindowRow(_ win: OcrWindowResult) -> some View {
|
|
2090
|
-
let isExpanded = expandedOcrWindow == win.wid
|
|
2091
|
-
let preview = String(win.fullText.prefix(80)).replacingOccurrences(of: "\n", with: " ")
|
|
2092
|
-
|
|
2093
|
-
return VStack(alignment: .leading, spacing: 0) {
|
|
2094
|
-
Button {
|
|
2095
|
-
withAnimation(.easeInOut(duration: 0.15)) {
|
|
2096
|
-
expandedOcrWindow = isExpanded ? nil : win.wid
|
|
2097
|
-
}
|
|
2098
|
-
} label: {
|
|
2099
|
-
HStack(spacing: 6) {
|
|
2100
|
-
Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
|
|
2101
|
-
.font(.system(size: 7, weight: .semibold))
|
|
2102
|
-
.foregroundColor(Palette.textMuted)
|
|
2103
|
-
.frame(width: 8)
|
|
2104
|
-
|
|
2105
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
2106
|
-
HStack(spacing: 6) {
|
|
2107
|
-
Text(win.title.isEmpty ? "Untitled" : win.title)
|
|
2108
|
-
.font(Typo.mono(10))
|
|
2109
|
-
.foregroundColor(Palette.text)
|
|
2110
|
-
.lineLimit(1)
|
|
2111
|
-
|
|
2112
|
-
Spacer()
|
|
2113
|
-
|
|
2114
|
-
Text(ocrRelativeTime(win.timestamp))
|
|
2115
|
-
.font(Typo.caption(9))
|
|
2116
|
-
.foregroundColor(Palette.textMuted)
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
if !isExpanded && !preview.isEmpty {
|
|
2120
|
-
Text(preview)
|
|
2121
|
-
.font(Typo.caption(9))
|
|
2122
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
2123
|
-
.lineLimit(1)
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
.padding(.vertical, 4)
|
|
2128
|
-
.contentShape(Rectangle())
|
|
2129
|
-
}
|
|
2130
|
-
.buttonStyle(.plain)
|
|
2131
|
-
|
|
2132
|
-
if isExpanded {
|
|
2133
|
-
ocrExpandedDetail(win)
|
|
2134
|
-
.padding(.leading, 14)
|
|
2135
|
-
.padding(.vertical, 4)
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
private func ocrExpandedDetail(_ win: OcrWindowResult) -> some View {
|
|
2141
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
2142
|
-
// Metadata row
|
|
2143
|
-
HStack(spacing: 10) {
|
|
2144
|
-
let avgConfidence = win.texts.isEmpty ? 0 : win.texts.map(\.confidence).reduce(0, +) / Float(win.texts.count)
|
|
2145
|
-
Text("\(win.texts.count) blocks")
|
|
2146
|
-
.font(Typo.caption(9))
|
|
2147
|
-
.foregroundColor(Palette.textMuted)
|
|
2148
|
-
Text("confidence: \(String(format: "%.0f%%", avgConfidence * 100))")
|
|
2149
|
-
.font(Typo.caption(9))
|
|
2150
|
-
.foregroundColor(Palette.textMuted)
|
|
2151
|
-
Spacer()
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
// Full text in scrollable monospaced area
|
|
2155
|
-
ScrollView {
|
|
2156
|
-
Text(win.fullText)
|
|
2157
|
-
.font(.system(size: 10, design: .monospaced))
|
|
2158
|
-
.foregroundColor(Palette.textDim)
|
|
2159
|
-
.textSelection(.enabled)
|
|
2160
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2161
|
-
.padding(8)
|
|
2162
|
-
}
|
|
2163
|
-
.frame(maxHeight: 150)
|
|
2164
|
-
.background(
|
|
2165
|
-
RoundedRectangle(cornerRadius: 4)
|
|
2166
|
-
.fill(Color.black.opacity(0.2))
|
|
2167
|
-
.overlay(
|
|
2168
|
-
RoundedRectangle(cornerRadius: 4)
|
|
2169
|
-
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
2170
|
-
)
|
|
2171
|
-
)
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
private func ocrRelativeTime(_ date: Date) -> String {
|
|
2176
|
-
let seconds = Int(-date.timeIntervalSinceNow)
|
|
2177
|
-
if seconds < 60 { return "just now" }
|
|
2178
|
-
let minutes = seconds / 60
|
|
2179
|
-
if minutes < 60 { return "\(minutes)m ago" }
|
|
2180
|
-
let hours = minutes / 60
|
|
2181
|
-
if hours < 24 { return "\(hours)h ago" }
|
|
2182
|
-
return "\(hours / 24)d ago"
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
// MARK: - Settings Card
|
|
2186
|
-
|
|
2187
|
-
private func settingsCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
|
2188
|
-
content()
|
|
2189
|
-
.padding(.horizontal, 14)
|
|
2190
|
-
.padding(.vertical, 12)
|
|
2191
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2192
|
-
.liquidGlass()
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
private var cardDivider: some View {
|
|
2196
|
-
Rectangle()
|
|
2197
|
-
.fill(
|
|
2198
|
-
LinearGradient(
|
|
2199
|
-
colors: [Color.white.opacity(0.03), Color.white.opacity(0.08), Color.white.opacity(0.03)],
|
|
2200
|
-
startPoint: .leading,
|
|
2201
|
-
endPoint: .trailing
|
|
2202
|
-
)
|
|
2203
|
-
)
|
|
2204
|
-
.frame(height: 0.5)
|
|
2205
|
-
.padding(.vertical, 3)
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
private func permissionSettingsRow(
|
|
2209
|
-
_ title: String,
|
|
2210
|
-
granted: Bool,
|
|
2211
|
-
detail: String,
|
|
2212
|
-
action: @escaping () -> Void
|
|
2213
|
-
) -> some View {
|
|
2214
|
-
Button {
|
|
2215
|
-
if granted {
|
|
2216
|
-
permChecker.check()
|
|
2217
|
-
} else {
|
|
2218
|
-
action()
|
|
2219
|
-
}
|
|
2220
|
-
} label: {
|
|
2221
|
-
permissionRowContent(
|
|
2222
|
-
title,
|
|
2223
|
-
status: granted ? "granted" : "not set",
|
|
2224
|
-
statusColor: granted ? Palette.running : Palette.detach,
|
|
2225
|
-
icon: granted ? "checkmark.circle.fill" : "exclamationmark.circle.fill",
|
|
2226
|
-
iconColor: granted ? Palette.running : Palette.detach,
|
|
2227
|
-
detail: detail,
|
|
2228
|
-
showsExternalLink: !granted
|
|
2229
|
-
)
|
|
2230
|
-
}
|
|
2231
|
-
.buttonStyle(.plain)
|
|
2232
|
-
.help(granted ? "Refresh permission status" : "Open macOS permission flow")
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
private func permissionReviewRow(
|
|
2236
|
-
_ title: String,
|
|
2237
|
-
detail: String,
|
|
2238
|
-
action: @escaping () -> Void
|
|
2239
|
-
) -> some View {
|
|
2240
|
-
Button(action: action) {
|
|
2241
|
-
permissionRowContent(
|
|
2242
|
-
title,
|
|
2243
|
-
status: "review",
|
|
2244
|
-
statusColor: Palette.textDim,
|
|
2245
|
-
icon: "gearshape.2.fill",
|
|
2246
|
-
iconColor: Palette.textDim,
|
|
2247
|
-
detail: detail,
|
|
2248
|
-
showsExternalLink: true
|
|
2249
|
-
)
|
|
2250
|
-
}
|
|
2251
|
-
.buttonStyle(.plain)
|
|
2252
|
-
.help("Open macOS Privacy & Security settings")
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
private func permissionRowContent(
|
|
2256
|
-
_ title: String,
|
|
2257
|
-
status: String,
|
|
2258
|
-
statusColor: Color,
|
|
2259
|
-
icon: String,
|
|
2260
|
-
iconColor: Color,
|
|
2261
|
-
detail: String,
|
|
2262
|
-
showsExternalLink: Bool
|
|
2263
|
-
) -> some View {
|
|
2264
|
-
HStack(alignment: .top, spacing: 8) {
|
|
2265
|
-
Image(systemName: icon)
|
|
2266
|
-
.font(.system(size: 10, weight: .semibold))
|
|
2267
|
-
.foregroundColor(iconColor)
|
|
2268
|
-
.frame(width: 12, height: 16)
|
|
2269
|
-
|
|
2270
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
2271
|
-
HStack(spacing: 6) {
|
|
2272
|
-
Text(title)
|
|
2273
|
-
.font(Typo.mono(10))
|
|
2274
|
-
.foregroundColor(Palette.text)
|
|
2275
|
-
Text(status)
|
|
2276
|
-
.font(Typo.mono(9))
|
|
2277
|
-
.foregroundColor(statusColor)
|
|
2278
|
-
Spacer()
|
|
2279
|
-
if showsExternalLink {
|
|
2280
|
-
Image(systemName: "arrow.up.forward.square")
|
|
2281
|
-
.font(.system(size: 9))
|
|
2282
|
-
.foregroundColor(Palette.textMuted)
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
Text(detail)
|
|
2287
|
-
.font(Typo.caption(9))
|
|
2288
|
-
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
2289
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
.padding(.horizontal, 8)
|
|
2293
|
-
.padding(.vertical, 7)
|
|
2294
|
-
.background(
|
|
2295
|
-
RoundedRectangle(cornerRadius: 5)
|
|
2296
|
-
.fill(Palette.surfaceHov.opacity(status == "not set" ? 0.75 : 0.35))
|
|
2297
|
-
.overlay(
|
|
2298
|
-
RoundedRectangle(cornerRadius: 5)
|
|
2299
|
-
.strokeBorder(status == "not set" ? Palette.detach.opacity(0.22) : Palette.borderLit.opacity(0.6), lineWidth: 0.5)
|
|
2300
|
-
)
|
|
2301
|
-
)
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
@ViewBuilder
|
|
2305
|
-
private func breakerStatusRow(
|
|
2306
|
-
state: EventTapBreaker.State,
|
|
2307
|
-
label: String,
|
|
2308
|
-
onReArm: @escaping () -> Void
|
|
2309
|
-
) -> some View {
|
|
2310
|
-
switch state {
|
|
2311
|
-
case .armed:
|
|
2312
|
-
EmptyView()
|
|
2313
|
-
case .paused(let cooldownSec):
|
|
2314
|
-
HStack(spacing: 8) {
|
|
2315
|
-
Circle()
|
|
2316
|
-
.fill(Color.orange)
|
|
2317
|
-
.frame(width: 6, height: 6)
|
|
2318
|
-
Text("\(label) paused — \(cooldownSec)s cooldown")
|
|
2319
|
-
.font(Typo.caption(9))
|
|
2320
|
-
.foregroundColor(Palette.textMuted)
|
|
2321
|
-
Spacer()
|
|
2322
|
-
}
|
|
2323
|
-
case .disabled:
|
|
2324
|
-
HStack(spacing: 8) {
|
|
2325
|
-
Circle()
|
|
2326
|
-
.fill(Color.red)
|
|
2327
|
-
.frame(width: 6, height: 6)
|
|
2328
|
-
Text("\(label) disabled — tap callback exceeded OS budget repeatedly")
|
|
2329
|
-
.font(Typo.caption(9))
|
|
2330
|
-
.foregroundColor(Palette.textMuted)
|
|
2331
|
-
Spacer()
|
|
2332
|
-
Button {
|
|
2333
|
-
onReArm()
|
|
2334
|
-
} label: {
|
|
2335
|
-
Text("Re-enable")
|
|
2336
|
-
.font(Typo.monoBold(10))
|
|
2337
|
-
.foregroundColor(Palette.text)
|
|
2338
|
-
.padding(.horizontal, 10)
|
|
2339
|
-
.padding(.vertical, 3)
|
|
2340
|
-
.background(
|
|
2341
|
-
RoundedRectangle(cornerRadius: 4)
|
|
2342
|
-
.fill(Palette.surfaceHov)
|
|
2343
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
2344
|
-
)
|
|
2345
|
-
}
|
|
2346
|
-
.buttonStyle(.plain)
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
// MARK: - Shortcuts
|
|
2352
|
-
|
|
2353
|
-
private var shortcutsContent: some View {
|
|
2354
|
-
VStack(spacing: 0) {
|
|
2355
|
-
GeometryReader { geo in
|
|
2356
|
-
let contentWidth = max(geo.size.width - 40, 320)
|
|
2357
|
-
let sectionColumns = [
|
|
2358
|
-
GridItem(.adaptive(minimum: min(320, contentWidth), maximum: 440), spacing: 16, alignment: .top)
|
|
2359
|
-
]
|
|
2360
|
-
let tilingColumns = contentWidth > 860
|
|
2361
|
-
? [
|
|
2362
|
-
GridItem(.flexible(minimum: 280, maximum: 360), spacing: 16, alignment: .top),
|
|
2363
|
-
GridItem(.flexible(minimum: 320, maximum: 640), spacing: 16, alignment: .top)
|
|
2364
|
-
]
|
|
2365
|
-
: [GridItem(.flexible(minimum: 0, maximum: .infinity), spacing: 16, alignment: .top)]
|
|
2366
|
-
|
|
2367
|
-
ScrollView {
|
|
2368
|
-
VStack(alignment: .leading, spacing: 0) {
|
|
2369
|
-
VStack(alignment: .leading, spacing: 16) {
|
|
2370
|
-
shortcutsOverviewCard
|
|
2371
|
-
inputControlsCard
|
|
2372
|
-
|
|
2373
|
-
LazyVGrid(columns: sectionColumns, alignment: .leading, spacing: 16) {
|
|
2374
|
-
shortcutsAppCard
|
|
2375
|
-
shortcutsLayersCard
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
shortcutSectionCard(
|
|
2379
|
-
title: "Window Tiling",
|
|
2380
|
-
eyebrow: "Desktop Layout",
|
|
2381
|
-
summary: "See the directional map first, then edit the matching global shortcuts below."
|
|
2382
|
-
) {
|
|
2383
|
-
LazyVGrid(columns: tilingColumns, alignment: .leading, spacing: 16) {
|
|
2384
|
-
shortcutsTilingVisualizer
|
|
2385
|
-
shortcutsTilingEditors
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
shortcutsTmuxCard
|
|
2390
|
-
}
|
|
2391
|
-
.padding(.horizontal, 20)
|
|
2392
|
-
.padding(.vertical, 16)
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
Spacer(minLength: 0)
|
|
2398
|
-
|
|
2399
|
-
separator
|
|
2400
|
-
|
|
2401
|
-
HStack {
|
|
2402
|
-
HStack(spacing: 8) {
|
|
2403
|
-
footerActionButton(icon: "book", label: "Docs") {
|
|
2404
|
-
ScreenMapWindowController.shared.showPage(.docs)
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
footerActionButton(icon: "stethoscope", label: "Diagnostics") {
|
|
2408
|
-
DiagnosticWindow.shared.show()
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
Spacer()
|
|
2413
|
-
|
|
2414
|
-
Button {
|
|
2415
|
-
hotkeyStore.resetAll()
|
|
2416
|
-
} label: {
|
|
2417
|
-
Text("Reset All to Defaults")
|
|
2418
|
-
.font(Typo.caption(11))
|
|
2419
|
-
.foregroundColor(Palette.textDim)
|
|
2420
|
-
.padding(.horizontal, 12)
|
|
2421
|
-
.padding(.vertical, 5)
|
|
2422
|
-
.background(
|
|
2423
|
-
RoundedRectangle(cornerRadius: 3)
|
|
2424
|
-
.fill(Palette.surface)
|
|
2425
|
-
.overlay(
|
|
2426
|
-
RoundedRectangle(cornerRadius: 3)
|
|
2427
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
2428
|
-
)
|
|
2429
|
-
)
|
|
2430
|
-
}
|
|
2431
|
-
.buttonStyle(.plain)
|
|
2432
|
-
}
|
|
2433
|
-
.padding(.horizontal, 20)
|
|
2434
|
-
.padding(.vertical, 10)
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
// MARK: - Shortcuts: Overview
|
|
2439
|
-
|
|
2440
|
-
private var companionCockpitCard: some View {
|
|
2441
|
-
let layout = LatticesCompanionCockpitCatalog.normalized(prefs.companionCockpitLayout)
|
|
2442
|
-
let selectedPage = layout.pages.first(where: { $0.id == selectedCompanionCockpitPageID }) ?? layout.pages.first
|
|
2443
|
-
let categories = LatticesCompanionShortcutCategory.allCases
|
|
2444
|
-
let trustedDeviceCount = companionTrustedDevices(revision: companionTrustRevision).count
|
|
2445
|
-
|
|
2446
|
-
return shortcutSectionCard(
|
|
2447
|
-
title: "Companion Cockpit",
|
|
2448
|
-
eyebrow: "iPad & iPhone",
|
|
2449
|
-
summary: "Define the Mac-authored command deck here, then let the companion app render it. Trackpad proxy runs through the same bridge."
|
|
2450
|
-
) {
|
|
2451
|
-
VStack(alignment: .leading, spacing: 14) {
|
|
2452
|
-
HStack(alignment: .top, spacing: 12) {
|
|
2453
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
2454
|
-
Text("Trackpad Proxy")
|
|
2455
|
-
.font(Typo.monoBold(11))
|
|
2456
|
-
.foregroundColor(Palette.text)
|
|
2457
|
-
Text("Enable remote pointer control for the iPad trackpad surface. Accessibility permission is still required on the Mac.")
|
|
2458
|
-
.font(Typo.caption(10.5))
|
|
2459
|
-
.foregroundColor(Palette.textMuted)
|
|
2460
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
Spacer()
|
|
2464
|
-
|
|
2465
|
-
Toggle("", isOn: $prefs.companionTrackpadEnabled)
|
|
2466
|
-
.toggleStyle(.switch)
|
|
2467
|
-
.labelsHidden()
|
|
2468
|
-
.disabled(!prefs.companionBridgeEnabled)
|
|
2469
|
-
.opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
|
-
HStack(alignment: .center, spacing: 12) {
|
|
2473
|
-
VStack(alignment: .leading, spacing: 4) {
|
|
2474
|
-
Text("Pairing and trust")
|
|
2475
|
-
.font(Typo.monoBold(11))
|
|
2476
|
-
.foregroundColor(Palette.text)
|
|
2477
|
-
Text("\(trustedDeviceCount) paired \(trustedDeviceCount == 1 ? "device" : "devices"). Revoke devices and review bridge grants in Companion settings.")
|
|
2478
|
-
.font(Typo.caption(10.5))
|
|
2479
|
-
.foregroundColor(Palette.textMuted)
|
|
2480
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
Spacer()
|
|
2484
|
-
|
|
2485
|
-
Button {
|
|
2486
|
-
selectedTab = .companion
|
|
2487
|
-
} label: {
|
|
2488
|
-
HStack(spacing: 5) {
|
|
2489
|
-
Image(systemName: "ipad.and.iphone")
|
|
2490
|
-
.font(.system(size: 10, weight: .semibold))
|
|
2491
|
-
Text("Manage")
|
|
2492
|
-
.font(Typo.monoBold(10))
|
|
2493
|
-
}
|
|
2494
|
-
.foregroundColor(Palette.text)
|
|
2495
|
-
.padding(.horizontal, 10)
|
|
2496
|
-
.padding(.vertical, 5)
|
|
2497
|
-
.background(
|
|
2498
|
-
RoundedRectangle(cornerRadius: 5)
|
|
2499
|
-
.fill(Palette.surfaceHov)
|
|
2500
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
2501
|
-
)
|
|
2502
|
-
}
|
|
2503
|
-
.buttonStyle(.plain)
|
|
2504
|
-
}
|
|
2505
|
-
.padding(12)
|
|
2506
|
-
.background(shortcutsInsetPanel)
|
|
2507
|
-
|
|
2508
|
-
if let selectedPage {
|
|
2509
|
-
Picker("Companion page", selection: $selectedCompanionCockpitPageID) {
|
|
2510
|
-
ForEach(layout.pages) { page in
|
|
2511
|
-
Text(page.title).tag(page.id)
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
.pickerStyle(.segmented)
|
|
2515
|
-
|
|
2516
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2517
|
-
if let subtitle = selectedPage.subtitle, !subtitle.isEmpty {
|
|
2518
|
-
Text(subtitle)
|
|
2519
|
-
.font(Typo.caption(10.5))
|
|
2520
|
-
.foregroundColor(Palette.textMuted)
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
LazyVGrid(
|
|
2524
|
-
columns: Array(
|
|
2525
|
-
repeating: GridItem(.flexible(minimum: 120, maximum: 220), spacing: 8, alignment: .top),
|
|
2526
|
-
count: max(2, selectedPage.columns)
|
|
2527
|
-
),
|
|
2528
|
-
alignment: .leading,
|
|
2529
|
-
spacing: 8
|
|
2530
|
-
) {
|
|
2531
|
-
ForEach(Array(selectedPage.slotIDs.enumerated()), id: \.offset) { index, shortcutID in
|
|
2532
|
-
companionCockpitSlotMenu(
|
|
2533
|
-
pageID: selectedPage.id,
|
|
2534
|
-
index: index,
|
|
2535
|
-
shortcutID: shortcutID,
|
|
2536
|
-
categories: categories
|
|
2537
|
-
)
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
.padding(12)
|
|
2542
|
-
.background(shortcutsInsetPanel)
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
HStack(spacing: 10) {
|
|
2546
|
-
Text("Changes appear in the iPad companion on the next snapshot refresh.")
|
|
2547
|
-
.font(Typo.caption(10.5))
|
|
2548
|
-
.foregroundColor(Palette.textMuted)
|
|
2549
|
-
|
|
2550
|
-
Spacer()
|
|
2551
|
-
|
|
2552
|
-
Button("Reset Companion Layout") {
|
|
2553
|
-
prefs.resetCompanionCockpitLayout()
|
|
2554
|
-
}
|
|
2555
|
-
.buttonStyle(.plain)
|
|
2556
|
-
.font(Typo.caption(10.5))
|
|
2557
|
-
.foregroundColor(Palette.textDim)
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
private var shortcutsOverviewCard: some View {
|
|
2564
|
-
shortcutSectionCard(
|
|
2565
|
-
title: "Shortcut Map",
|
|
2566
|
-
eyebrow: "Quick Reference",
|
|
2567
|
-
summary: "Global hotkeys are editable here. tmux shortcuts stay as a built-in reference so you can keep your workspace flow in one place."
|
|
2568
|
-
) {
|
|
2569
|
-
LazyVGrid(
|
|
2570
|
-
columns: [GridItem(.adaptive(minimum: 180, maximum: 240), spacing: 10, alignment: .top)],
|
|
2571
|
-
alignment: .leading,
|
|
2572
|
-
spacing: 10
|
|
2573
|
-
) {
|
|
2574
|
-
shortcutFactCard(
|
|
2575
|
-
icon: "command",
|
|
2576
|
-
title: "Global Hotkeys",
|
|
2577
|
-
detail: "Edit palette, search, voice, and workspace actions without leaving settings."
|
|
2578
|
-
)
|
|
2579
|
-
shortcutFactCard(
|
|
2580
|
-
icon: "rectangle.split.3x3",
|
|
2581
|
-
title: "Spatial Tiling",
|
|
2582
|
-
detail: "The layout grid mirrors the screen positions used by the menu bar app."
|
|
2583
|
-
)
|
|
2584
|
-
shortcutFactCard(
|
|
2585
|
-
icon: "terminal",
|
|
2586
|
-
title: "tmux Muscle Memory",
|
|
2587
|
-
detail: "Keep the core pane controls visible here while you tune the app-level shortcuts."
|
|
2588
|
-
)
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
// MARK: - Shortcuts: App
|
|
2594
|
-
|
|
2595
|
-
private var shortcutsAppCard: some View {
|
|
2596
|
-
shortcutSectionCard(
|
|
2597
|
-
title: "App & Workspace",
|
|
2598
|
-
eyebrow: "Global",
|
|
2599
|
-
summary: "Commands for opening primary surfaces and navigating the desktop companion."
|
|
2600
|
-
) {
|
|
2601
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2602
|
-
ForEach(HotkeyAction.allCases.filter { $0.group == .app }, id: \.rawValue) { action in
|
|
2603
|
-
compactKeyRecorder(action: action)
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
// MARK: - Shortcuts: Layers
|
|
2610
|
-
|
|
2611
|
-
private var shortcutsLayersCard: some View {
|
|
2612
|
-
shortcutSectionCard(
|
|
2613
|
-
title: "Layers",
|
|
2614
|
-
eyebrow: "Workspace Stack",
|
|
2615
|
-
summary: "Direct jumps stay grouped separately from layer cycling so the numeric map is easier to scan."
|
|
2616
|
-
) {
|
|
2617
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
2618
|
-
shortcutSubsectionLabel("Jump to a Layer")
|
|
2619
|
-
|
|
2620
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2621
|
-
ForEach(HotkeyAction.layerActions, id: \.rawValue) { action in
|
|
2622
|
-
compactKeyRecorder(action: action)
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
cardDivider
|
|
2627
|
-
|
|
2628
|
-
shortcutSubsectionLabel("Cycle & Tag")
|
|
2629
|
-
|
|
2630
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2631
|
-
ForEach([HotkeyAction.layerPrev, .layerNext, .layerTag], id: \.rawValue) { action in
|
|
2632
|
-
compactKeyRecorder(action: action)
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
// MARK: - Shortcuts: Tiling
|
|
2640
|
-
|
|
2641
|
-
private var shortcutsTilingVisualizer: some View {
|
|
2642
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
2643
|
-
shortcutSubsectionLabel("Screen Regions")
|
|
2644
|
-
|
|
2645
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
2646
|
-
VStack(spacing: 2) {
|
|
2647
|
-
HStack(spacing: 2) {
|
|
2648
|
-
tileCell(action: .tileTopLeft, label: "TL")
|
|
2649
|
-
tileCell(action: .tileTop, label: "Top")
|
|
2650
|
-
tileCell(action: .tileTopRight, label: "TR")
|
|
2651
|
-
}
|
|
2652
|
-
HStack(spacing: 2) {
|
|
2653
|
-
tileCell(action: .tileLeft, label: "Left")
|
|
2654
|
-
tileCell(action: .tileMaximize, label: "Max")
|
|
2655
|
-
tileCell(action: .tileRight, label: "Right")
|
|
2656
|
-
}
|
|
2657
|
-
HStack(spacing: 2) {
|
|
2658
|
-
tileCell(action: .tileBottomLeft, label: "BL")
|
|
2659
|
-
tileCell(action: .tileBottom, label: "Bottom")
|
|
2660
|
-
tileCell(action: .tileBottomRight, label: "BR")
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
.padding(8)
|
|
2664
|
-
.background(shortcutsInsetPanel)
|
|
2665
|
-
|
|
2666
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
2667
|
-
Text("Thirds")
|
|
2668
|
-
.font(Typo.caption(10.5))
|
|
2669
|
-
.foregroundColor(Palette.textMuted)
|
|
2670
|
-
|
|
2671
|
-
HStack(spacing: 2) {
|
|
2672
|
-
tileCell(action: .tileLeftThird, label: "\u{2153}L")
|
|
2673
|
-
tileCell(action: .tileCenterThird, label: "\u{2153}C")
|
|
2674
|
-
tileCell(action: .tileRightThird, label: "\u{2153}R")
|
|
2675
|
-
}
|
|
2676
|
-
}
|
|
2677
|
-
.padding(8)
|
|
2678
|
-
.background(shortcutsInsetPanel)
|
|
2679
|
-
|
|
2680
|
-
Text("Use the grid as a visual legend for where each shortcut will place the focused window.")
|
|
2681
|
-
.font(Typo.caption(10.5))
|
|
2682
|
-
.foregroundColor(Palette.textMuted)
|
|
2683
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
private var shortcutsTilingEditors: some View {
|
|
2689
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
2690
|
-
shortcutSubsectionLabel("Editable Bindings")
|
|
2691
|
-
|
|
2692
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2693
|
-
ForEach([
|
|
2694
|
-
HotkeyAction.tileLeft, .tileRight, .tileTop, .tileBottom,
|
|
2695
|
-
.tileTopLeft, .tileTopRight, .tileBottomLeft, .tileBottomRight
|
|
2696
|
-
], id: \.rawValue) { action in
|
|
2697
|
-
compactKeyRecorder(action: action)
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
cardDivider
|
|
2702
|
-
|
|
2703
|
-
shortcutSubsectionLabel("Layout Helpers")
|
|
2704
|
-
|
|
2705
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2706
|
-
ForEach([
|
|
2707
|
-
HotkeyAction.tileLeftThird, .tileCenterThird, .tileRightThird,
|
|
2708
|
-
.tileCenter, .tileMaximize, .tileDistribute, .tileTypeGrid
|
|
2709
|
-
], id: \.rawValue) { action in
|
|
2710
|
-
compactKeyRecorder(action: action)
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
// MARK: - Shortcuts: tmux
|
|
2717
|
-
|
|
2718
|
-
private var shortcutsTmuxCard: some View {
|
|
2719
|
-
shortcutSectionCard(
|
|
2720
|
-
title: "Inside tmux",
|
|
2721
|
-
eyebrow: "Reference",
|
|
2722
|
-
summary: "These are tmux-native controls. They are shown here for fast recall and are not edited by the app."
|
|
2723
|
-
) {
|
|
2724
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
2725
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2726
|
-
shortcutRow("Detach", keys: ["Ctrl+B", "D"])
|
|
2727
|
-
shortcutRow("Kill pane", keys: ["Ctrl+B", "X"])
|
|
2728
|
-
shortcutRow("Pane left", keys: ["Ctrl+B", "\u{2190}"])
|
|
2729
|
-
shortcutRow("Pane right", keys: ["Ctrl+B", "\u{2192}"])
|
|
2730
|
-
shortcutRow("Zoom toggle", keys: ["Ctrl+B", "Z"])
|
|
2731
|
-
shortcutRow("Scroll mode", keys: ["Ctrl+B", "["])
|
|
2732
|
-
}
|
|
2733
|
-
.padding(12)
|
|
2734
|
-
.background(shortcutsInsetPanel)
|
|
2735
|
-
|
|
2736
|
-
Text("Tip: use this as your quick memory jogger while editing the global shortcuts above.")
|
|
2737
|
-
.font(Typo.caption(10.5))
|
|
2738
|
-
.foregroundColor(Palette.textMuted)
|
|
2739
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
compactKeyRecorder(action: .tileOrganize)
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
// MARK: - Shortcut section UI
|
|
2747
|
-
|
|
2748
|
-
private func shortcutSectionCard<Content: View>(
|
|
2749
|
-
title: String,
|
|
2750
|
-
eyebrow: String,
|
|
2751
|
-
summary: String,
|
|
2752
|
-
@ViewBuilder content: () -> Content
|
|
2753
|
-
) -> some View {
|
|
2754
|
-
settingsCard {
|
|
2755
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
2756
|
-
VStack(alignment: .leading, spacing: 5) {
|
|
2757
|
-
Text(eyebrow.uppercased())
|
|
2758
|
-
.font(Typo.pixel(12))
|
|
2759
|
-
.foregroundColor(Palette.textDim)
|
|
2760
|
-
.tracking(1)
|
|
2761
|
-
|
|
2762
|
-
Text(title)
|
|
2763
|
-
.font(Typo.monoBold(12))
|
|
2764
|
-
.foregroundColor(Palette.text)
|
|
2765
|
-
|
|
2766
|
-
Text(summary)
|
|
2767
|
-
.font(Typo.caption(10.5))
|
|
2768
|
-
.foregroundColor(Palette.textMuted)
|
|
2769
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
content()
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
private func shortcutFactCard(icon: String, title: String, detail: String) -> some View {
|
|
2778
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2779
|
-
Image(systemName: icon)
|
|
2780
|
-
.font(.system(size: 12, weight: .semibold))
|
|
2781
|
-
.foregroundColor(Palette.textDim)
|
|
2782
|
-
|
|
2783
|
-
Text(title)
|
|
2784
|
-
.font(Typo.monoBold(11))
|
|
2785
|
-
.foregroundColor(Palette.text)
|
|
2786
|
-
|
|
2787
|
-
Text(detail)
|
|
2788
|
-
.font(Typo.caption(10))
|
|
2789
|
-
.foregroundColor(Palette.textMuted)
|
|
2790
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2791
|
-
}
|
|
2792
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2793
|
-
.padding(12)
|
|
2794
|
-
.background(shortcutsInsetPanel)
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
private func shortcutSubsectionLabel(_ title: String) -> some View {
|
|
2798
|
-
Text(title.uppercased())
|
|
2799
|
-
.font(Typo.pixel(11))
|
|
2800
|
-
.foregroundColor(Palette.textDim)
|
|
2801
|
-
.tracking(1)
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
private var shortcutsInsetPanel: some View {
|
|
2805
|
-
RoundedRectangle(cornerRadius: 8)
|
|
2806
|
-
.fill(Color.black.opacity(0.22))
|
|
2807
|
-
.overlay(
|
|
2808
|
-
RoundedRectangle(cornerRadius: 8)
|
|
2809
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
2810
|
-
)
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
private func relativeTimestamp(_ date: Date) -> String {
|
|
2814
|
-
RelativeDateTimeFormatter().localizedString(for: date, relativeTo: Date())
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
private func companionTrustedDevices(revision: Int) -> [DeckTrustedDeviceSummary] {
|
|
2818
|
-
_ = revision
|
|
2819
|
-
return LatticesCompanionSecurityCoordinator.shared.trustedDeviceSummaries()
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
// MARK: - Tile cell (spatial grid item)
|
|
2823
|
-
|
|
2824
|
-
private func tileCell(action: HotkeyAction, label: String) -> some View {
|
|
2825
|
-
let binding = hotkeyStore.bindings[action]
|
|
2826
|
-
let badgeText = binding?.displayParts.last ?? ""
|
|
2827
|
-
|
|
2828
|
-
return Button {
|
|
2829
|
-
// Open inline key recorder for this action
|
|
2830
|
-
} label: {
|
|
2831
|
-
VStack(spacing: 3) {
|
|
2832
|
-
Text(label)
|
|
2833
|
-
.font(Typo.caption(9))
|
|
2834
|
-
.foregroundColor(Palette.textDim)
|
|
2835
|
-
Text(badgeText)
|
|
2836
|
-
.font(Typo.geistMonoBold(9))
|
|
2837
|
-
.foregroundColor(Palette.text)
|
|
2838
|
-
}
|
|
2839
|
-
.frame(maxWidth: .infinity)
|
|
2840
|
-
.frame(height: 42)
|
|
2841
|
-
.background(
|
|
2842
|
-
RoundedRectangle(cornerRadius: 4)
|
|
2843
|
-
.fill(Palette.surface)
|
|
2844
|
-
.overlay(
|
|
2845
|
-
RoundedRectangle(cornerRadius: 4)
|
|
2846
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
2847
|
-
)
|
|
2848
|
-
)
|
|
2849
|
-
}
|
|
2850
|
-
.buttonStyle(.plain)
|
|
2851
|
-
.popover(isPresented: tileCellPopoverBinding(for: action)) {
|
|
2852
|
-
KeyRecorderView(action: action, store: hotkeyStore)
|
|
2853
|
-
.padding(12)
|
|
2854
|
-
.frame(width: 300)
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
@State private var expandedOcrWindow: UInt32?
|
|
2859
|
-
@State private var collapsedOcrApps: Set<String> = []
|
|
2860
|
-
|
|
2861
|
-
@State private var activeTilePopover: HotkeyAction?
|
|
2862
|
-
@State private var selectedCompanionCockpitPageID = "main"
|
|
2863
|
-
@State private var companionTrustRevision = 0
|
|
2864
|
-
|
|
2865
|
-
private func tileCellPopoverBinding(for action: HotkeyAction) -> Binding<Bool> {
|
|
2866
|
-
Binding(
|
|
2867
|
-
get: { activeTilePopover == action },
|
|
2868
|
-
set: { if !$0 { activeTilePopover = nil } }
|
|
2869
|
-
)
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
// MARK: - Compact key recorder
|
|
2873
|
-
|
|
2874
|
-
private func compactKeyRecorder(action: HotkeyAction) -> some View {
|
|
2875
|
-
KeyRecorderView(action: action, store: hotkeyStore)
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
private func companionCockpitSlotMenu(
|
|
2879
|
-
pageID: String,
|
|
2880
|
-
index: Int,
|
|
2881
|
-
shortcutID: String,
|
|
2882
|
-
categories: [LatticesCompanionShortcutCategory]
|
|
2883
|
-
) -> some View {
|
|
2884
|
-
let definition = LatticesCompanionCockpitCatalog.definition(for: shortcutID)
|
|
2885
|
-
let label = definition?.title ?? "Empty"
|
|
2886
|
-
let subtitle = definition?.subtitle ?? "Choose a shortcut"
|
|
2887
|
-
let icon = definition?.iconSystemName ?? "square.dashed"
|
|
2888
|
-
|
|
2889
|
-
return Menu {
|
|
2890
|
-
Button("Empty Slot") {
|
|
2891
|
-
prefs.updateCompanionCockpitSlot(pageID: pageID, index: index, shortcutID: "")
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
ForEach(categories) { category in
|
|
2895
|
-
let shortcuts = LatticesCompanionCockpitCatalog.shortcuts.filter {
|
|
2896
|
-
$0.category == category && !$0.id.isEmpty
|
|
2897
|
-
}
|
|
2898
|
-
if !shortcuts.isEmpty {
|
|
2899
|
-
Section(category.title) {
|
|
2900
|
-
ForEach(shortcuts) { shortcut in
|
|
2901
|
-
Button {
|
|
2902
|
-
prefs.updateCompanionCockpitSlot(
|
|
2903
|
-
pageID: pageID,
|
|
2904
|
-
index: index,
|
|
2905
|
-
shortcutID: shortcut.id
|
|
2906
|
-
)
|
|
2907
|
-
} label: {
|
|
2908
|
-
Label(shortcut.title, systemImage: shortcut.iconSystemName)
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
} label: {
|
|
2915
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
2916
|
-
HStack(alignment: .top) {
|
|
2917
|
-
Text("Slot \(index + 1)")
|
|
2918
|
-
.font(Typo.pixel(10))
|
|
2919
|
-
.foregroundColor(Palette.textDim)
|
|
2920
|
-
Spacer(minLength: 0)
|
|
2921
|
-
Image(systemName: "chevron.up.chevron.down")
|
|
2922
|
-
.font(.system(size: 10, weight: .semibold))
|
|
2923
|
-
.foregroundColor(Palette.textMuted)
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
Image(systemName: icon)
|
|
2927
|
-
.font(.system(size: 14, weight: .semibold))
|
|
2928
|
-
.foregroundColor(Palette.textDim)
|
|
2929
|
-
|
|
2930
|
-
Text(label)
|
|
2931
|
-
.font(Typo.monoBold(11))
|
|
2932
|
-
.foregroundColor(Palette.text)
|
|
2933
|
-
.lineLimit(2)
|
|
2934
|
-
|
|
2935
|
-
Text(subtitle)
|
|
2936
|
-
.font(Typo.caption(9.5))
|
|
2937
|
-
.foregroundColor(Palette.textMuted)
|
|
2938
|
-
.lineLimit(3)
|
|
2939
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2940
|
-
}
|
|
2941
|
-
.frame(maxWidth: .infinity, minHeight: 112, alignment: .topLeading)
|
|
2942
|
-
.padding(10)
|
|
2943
|
-
.background(
|
|
2944
|
-
RoundedRectangle(cornerRadius: 8)
|
|
2945
|
-
.fill(Palette.surface)
|
|
2946
|
-
.overlay(
|
|
2947
|
-
RoundedRectangle(cornerRadius: 8)
|
|
2948
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
2949
|
-
)
|
|
2950
|
-
)
|
|
2951
|
-
}
|
|
2952
|
-
.buttonStyle(.plain)
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
// MARK: - Shortcut row (read-only, for tmux)
|
|
2956
|
-
|
|
2957
|
-
private func shortcutRow(_ label: String, keys: [String]) -> some View {
|
|
2958
|
-
HStack {
|
|
2959
|
-
Text(label)
|
|
2960
|
-
.font(Typo.caption(11))
|
|
2961
|
-
.foregroundColor(Palette.textDim)
|
|
2962
|
-
.frame(width: 80, alignment: .trailing)
|
|
2963
|
-
|
|
2964
|
-
HStack(spacing: 4) {
|
|
2965
|
-
ForEach(keys, id: \.self) { key in
|
|
2966
|
-
keyBadge(key)
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
.padding(.leading, 8)
|
|
2970
|
-
|
|
2971
|
-
Spacer()
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
|
-
// MARK: - Docs
|
|
2976
|
-
|
|
2977
|
-
private var docsContent: some View {
|
|
2978
|
-
ScrollView {
|
|
2979
|
-
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: .sectionHeaders) {
|
|
2980
|
-
Section(header: stickyHeader("What is lattices?")) {
|
|
2981
|
-
Text("A developer workspace launcher. It creates pre-configured terminal layouts for your projects using tmux \u{2014} go from \u{201C}I want to work on X\u{201D} to a full environment in one click.")
|
|
2982
|
-
.font(Typo.caption(11))
|
|
2983
|
-
.foregroundColor(Palette.textDim)
|
|
2984
|
-
.lineSpacing(3)
|
|
2985
|
-
.padding(.horizontal, 20)
|
|
2986
|
-
.padding(.vertical, 12)
|
|
2987
|
-
}
|
|
2988
|
-
|
|
2989
|
-
Section(header: stickyHeader("Glossary")) {
|
|
2990
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
2991
|
-
glossaryItem("Session",
|
|
2992
|
-
"A persistent workspace that lives in the background. Survives terminal crashes, disconnects, even closing your laptop.")
|
|
2993
|
-
glossaryItem("Pane",
|
|
2994
|
-
"A single terminal view inside a session. A typical setup has two panes \u{2014} Claude Code on the left, dev server on the right.")
|
|
2995
|
-
glossaryItem("Attach",
|
|
2996
|
-
"Connect your terminal window to an existing session. The session was already running \u{2014} you\u{2019}re just viewing it.")
|
|
2997
|
-
glossaryItem("Detach",
|
|
2998
|
-
"Disconnect your terminal but keep the session alive. Your dev server keeps running, Claude keeps thinking.")
|
|
2999
|
-
glossaryItem("tmux",
|
|
3000
|
-
"Terminal multiplexer \u{2014} the engine behind lattices. It manages sessions, panes, and layouts. lattices configures it so you don\u{2019}t have to.")
|
|
3001
|
-
}
|
|
3002
|
-
.padding(.horizontal, 20)
|
|
3003
|
-
.padding(.vertical, 12)
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
Section(header: stickyHeader("How it works")) {
|
|
3007
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
3008
|
-
flowStep("1", "Create a .lattices.json in your project root")
|
|
3009
|
-
flowStep("2", "lattices reads the config and builds a tmux session")
|
|
3010
|
-
flowStep("3", "Each pane gets its command (claude, dev server, etc.)")
|
|
3011
|
-
flowStep("4", "Session persists in the background until you kill it")
|
|
3012
|
-
flowStep("5", "Attach and detach from any terminal, any time")
|
|
3013
|
-
}
|
|
3014
|
-
.padding(.horizontal, 20)
|
|
3015
|
-
.padding(.vertical, 12)
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
Section(header: stickyHeader("Voice commands")) {
|
|
3019
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
3020
|
-
flowStep("⌥", "Hold Option key to speak, release to stop")
|
|
3021
|
-
flowStep("⇥", "Tab to arm/disarm the mic")
|
|
3022
|
-
flowStep("⎋", "Escape to dismiss")
|
|
3023
|
-
|
|
3024
|
-
Text("Built-in commands: find, show, open, tile, kill, scan")
|
|
3025
|
-
.font(Typo.caption(10.5))
|
|
3026
|
-
.foregroundColor(Palette.textMuted)
|
|
3027
|
-
.padding(.top, 4)
|
|
3028
|
-
|
|
3029
|
-
Text("When local matching fails, Claude Haiku advises with follow-up suggestions. Configure the AI model and budget in Settings → AI.")
|
|
3030
|
-
.font(Typo.caption(10.5))
|
|
3031
|
-
.foregroundColor(Palette.textMuted)
|
|
3032
|
-
.lineSpacing(2)
|
|
3033
|
-
}
|
|
3034
|
-
.padding(.horizontal, 20)
|
|
3035
|
-
.padding(.vertical, 12)
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
Section(header: stickyHeader("Reference")) {
|
|
3039
|
-
HStack(spacing: 8) {
|
|
3040
|
-
docsLinkButton(icon: "doc.text", label: "Config format", file: "config.md")
|
|
3041
|
-
docsLinkButton(icon: "book", label: "Full concepts", file: "concepts.md")
|
|
3042
|
-
footerActionButton(icon: "stethoscope", label: "Diagnostics") {
|
|
3043
|
-
DiagnosticWindow.shared.show()
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
.padding(.horizontal, 20)
|
|
3047
|
-
.padding(.vertical, 12)
|
|
3048
|
-
}
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
// MARK: - Docs helpers
|
|
3054
|
-
|
|
3055
|
-
private func glossaryItem(_ term: String, _ definition: String) -> some View {
|
|
3056
|
-
VStack(alignment: .leading, spacing: 3) {
|
|
3057
|
-
Text(term)
|
|
3058
|
-
.font(Typo.monoBold(11))
|
|
3059
|
-
.foregroundColor(Palette.text)
|
|
3060
|
-
Text(definition)
|
|
3061
|
-
.font(Typo.caption(10.5))
|
|
3062
|
-
.foregroundColor(Palette.textMuted)
|
|
3063
|
-
.lineSpacing(2)
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
private func flowStep(_ number: String, _ text: String) -> some View {
|
|
3068
|
-
HStack(alignment: .top, spacing: 8) {
|
|
3069
|
-
Text(number)
|
|
3070
|
-
.font(Typo.monoBold(10))
|
|
3071
|
-
.foregroundColor(Palette.running)
|
|
3072
|
-
.frame(width: 14)
|
|
3073
|
-
Text(text)
|
|
3074
|
-
.font(Typo.caption(11))
|
|
3075
|
-
.foregroundColor(Palette.textDim)
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
private func docsLinkButton(icon: String, label: String, file: String) -> some View {
|
|
3080
|
-
Button {
|
|
3081
|
-
let path = resolveDocsFile(file)
|
|
3082
|
-
NSWorkspace.shared.open(URL(fileURLWithPath: path))
|
|
3083
|
-
} label: {
|
|
3084
|
-
footerActionLabel(icon: icon, label: label)
|
|
3085
|
-
}
|
|
3086
|
-
.buttonStyle(.plain)
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
private func footerActionButton(icon: String, label: String, action: @escaping () -> Void) -> some View {
|
|
3090
|
-
Button(action: action) {
|
|
3091
|
-
footerActionLabel(icon: icon, label: label)
|
|
3092
|
-
}
|
|
3093
|
-
.buttonStyle(.plain)
|
|
3094
|
-
}
|
|
3095
|
-
|
|
3096
|
-
private func footerActionLabel(icon: String, label: String) -> some View {
|
|
3097
|
-
HStack(spacing: 6) {
|
|
3098
|
-
Image(systemName: icon)
|
|
3099
|
-
.font(.system(size: 10))
|
|
3100
|
-
Text(label)
|
|
3101
|
-
.font(Typo.caption(11))
|
|
3102
|
-
}
|
|
3103
|
-
.foregroundColor(Palette.textDim)
|
|
3104
|
-
.padding(.horizontal, 12)
|
|
3105
|
-
.padding(.vertical, 6)
|
|
3106
|
-
.background(
|
|
3107
|
-
RoundedRectangle(cornerRadius: 3)
|
|
3108
|
-
.fill(Palette.surface)
|
|
3109
|
-
.overlay(
|
|
3110
|
-
RoundedRectangle(cornerRadius: 3)
|
|
3111
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
3112
|
-
)
|
|
3113
|
-
)
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
private func resolveDocsFile(_ file: String) -> String {
|
|
3117
|
-
let bundle = Bundle.main.bundlePath
|
|
3118
|
-
let appDir = (bundle as NSString).deletingLastPathComponent
|
|
3119
|
-
let docsPath = ((appDir as NSString).appendingPathComponent("../docs/\(file)") as NSString).standardizingPath
|
|
3120
|
-
if FileManager.default.fileExists(atPath: docsPath) { return docsPath }
|
|
3121
|
-
// Fallback: look relative to the repo root (dev builds)
|
|
3122
|
-
let repoGuess = ((appDir as NSString).appendingPathComponent("../../docs/\(file)") as NSString).standardizingPath
|
|
3123
|
-
return FileManager.default.fileExists(atPath: repoGuess) ? repoGuess : docsPath
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// MARK: - Shared helpers
|
|
3127
|
-
|
|
3128
|
-
private var separator: some View {
|
|
3129
|
-
Rectangle()
|
|
3130
|
-
.fill(Palette.border)
|
|
3131
|
-
.frame(height: 0.5)
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
private func settingsRow<Content: View>(_ label: String, @ViewBuilder content: () -> Content) -> some View {
|
|
3135
|
-
HStack(alignment: .top, spacing: 0) {
|
|
3136
|
-
Text(label)
|
|
3137
|
-
.font(Typo.caption(11))
|
|
3138
|
-
.foregroundColor(Palette.textDim)
|
|
3139
|
-
.frame(width: 100, alignment: .trailing)
|
|
3140
|
-
.padding(.top, 2)
|
|
3141
|
-
|
|
3142
|
-
content()
|
|
3143
|
-
.padding(.leading, 16)
|
|
3144
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
private func keyBadge(_ key: String) -> some View {
|
|
3149
|
-
Text(key)
|
|
3150
|
-
.font(Typo.geistMonoBold(10))
|
|
3151
|
-
.foregroundColor(Palette.text)
|
|
3152
|
-
.padding(.horizontal, 6)
|
|
3153
|
-
.padding(.vertical, 3)
|
|
3154
|
-
.background(
|
|
3155
|
-
RoundedRectangle(cornerRadius: 3)
|
|
3156
|
-
.fill(Palette.surface)
|
|
3157
|
-
.overlay(
|
|
3158
|
-
RoundedRectangle(cornerRadius: 3)
|
|
3159
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
3160
|
-
)
|
|
3161
|
-
)
|
|
3162
|
-
}
|
|
3163
|
-
}
|