@lattices/cli 0.4.9 → 0.4.11
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/LICENSE +21 -0
- package/README.md +13 -13
- package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
- package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/{app → apps/mac}/Package.swift +2 -1
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
- package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
- package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
- package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
- package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
- package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
- package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
- package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
- package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
- package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
- package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
- package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
- package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
- package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
- package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
- package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
- package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
- package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
- package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
- package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
- package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
- package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
- package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
- package/apps/mac/Sources/Core/System/Capability.swift +79 -0
- package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
- package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
- package/bin/handsoff-infer.ts +14 -5
- package/bin/handsoff-worker.ts +11 -7
- package/bin/infer.ts +406 -0
- package/bin/lattices-app.ts +57 -7
- package/bin/lattices-dev +40 -1
- package/bin/lattices.ts +1 -1
- package/docs/agent-execution-plan.md +9 -9
- package/docs/api.md +119 -0
- package/docs/app.md +1 -0
- package/docs/companion-deck.md +1 -1
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/mouse-gestures.md +79 -0
- package/docs/overview.md +2 -2
- package/docs/presentation-execution-review.md +9 -9
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/repo-structure.md +100 -0
- package/docs/voice-error-model.md +7 -7
- package/docs/voice.md +18 -0
- package/package.json +23 -13
- package/swift/Package.swift +20 -0
- package/swift/Sources/DeckKit/DeckAction.swift +51 -0
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
- package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
- package/swift/Sources/DeckKit/DeckHost.swift +7 -0
- package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
- package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
- package/swift/Sources/DeckKit/DeckValue.swift +93 -0
- package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
- package/app/Sources/AppShell/AppDelegate.swift +0 -408
- package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
- package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
- package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
- /package/{app → apps/mac}/Info.plist +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
- /package/{app → apps/mac}/Lattices.entitlements +0 -0
- /package/{app → apps/mac}/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
- /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
|
@@ -6,55 +6,55 @@ import SwiftUI
|
|
|
6
6
|
struct SettingsContentView: View {
|
|
7
7
|
private enum SettingsSection: String, CaseIterable, Identifiable {
|
|
8
8
|
case general
|
|
9
|
-
case
|
|
9
|
+
case shortcuts
|
|
10
10
|
case ai
|
|
11
11
|
case search
|
|
12
|
-
case
|
|
12
|
+
case companion
|
|
13
13
|
|
|
14
14
|
var id: String { rawValue }
|
|
15
15
|
|
|
16
16
|
var title: String {
|
|
17
17
|
switch self {
|
|
18
18
|
case .general: return "General"
|
|
19
|
-
case .
|
|
19
|
+
case .shortcuts: return "Shortcuts"
|
|
20
20
|
case .ai: return "AI"
|
|
21
21
|
case .search: return "Search & OCR"
|
|
22
|
-
case .
|
|
22
|
+
case .companion: return "LATS iOS Companion"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
var icon: String {
|
|
27
27
|
switch self {
|
|
28
28
|
case .general: return "slider.horizontal.3"
|
|
29
|
-
case .
|
|
29
|
+
case .shortcuts: return "command"
|
|
30
30
|
case .ai: return "sparkles"
|
|
31
31
|
case .search: return "text.viewfinder"
|
|
32
|
-
case .
|
|
32
|
+
case .companion: return "ipad.and.iphone"
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
var eyebrow: String {
|
|
37
37
|
switch self {
|
|
38
38
|
case .general: return "Workspace"
|
|
39
|
-
case .
|
|
39
|
+
case .shortcuts: return "Controls"
|
|
40
40
|
case .ai: return "Agents"
|
|
41
41
|
case .search: return "Indexing"
|
|
42
|
-
case .
|
|
42
|
+
case .companion: return "Local Bridge"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
var summary: String {
|
|
47
47
|
switch self {
|
|
48
48
|
case .general:
|
|
49
|
-
return "
|
|
50
|
-
case .
|
|
51
|
-
return "
|
|
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
52
|
case .ai:
|
|
53
53
|
return "Claude CLI detection plus advisor model and spending controls."
|
|
54
54
|
case .search:
|
|
55
55
|
return "OCR cadence, quality, and recent capture visibility."
|
|
56
|
-
case .
|
|
57
|
-
return "
|
|
56
|
+
case .companion:
|
|
57
|
+
return "Local-network pairing, trusted iPad devices, and bridge security."
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -67,6 +67,9 @@ struct SettingsContentView: View {
|
|
|
67
67
|
@ObservedObject var appUpdater: AppUpdater = .shared
|
|
68
68
|
@ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
|
|
69
69
|
@ObservedObject var keyboardRemapStore: KeyboardRemapStore = .shared
|
|
70
|
+
@ObservedObject var permChecker: PermissionChecker = .shared
|
|
71
|
+
@ObservedObject var mouseGestureController: MouseGestureController = .shared
|
|
72
|
+
@ObservedObject var keyboardRemapController: KeyboardRemapController = .shared
|
|
70
73
|
var onBack: (() -> Void)? = nil
|
|
71
74
|
|
|
72
75
|
@State private var selectedTab: SettingsSection = .general
|
|
@@ -86,10 +89,17 @@ struct SettingsContentView: View {
|
|
|
86
89
|
.clipped()
|
|
87
90
|
.background(PanelBackground())
|
|
88
91
|
.onAppear {
|
|
92
|
+
permChecker.check()
|
|
89
93
|
if page == .companionSettings {
|
|
90
94
|
selectedTab = .companion
|
|
91
95
|
}
|
|
92
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
|
+
}
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
// MARK: - Back Bar
|
|
@@ -138,7 +148,7 @@ struct SettingsContentView: View {
|
|
|
138
148
|
private var settingsBody: some View {
|
|
139
149
|
HStack(spacing: 0) {
|
|
140
150
|
settingsSidebar
|
|
141
|
-
.frame(width:
|
|
151
|
+
.frame(width: 190, alignment: .top)
|
|
142
152
|
|
|
143
153
|
Rectangle()
|
|
144
154
|
.fill(Palette.border)
|
|
@@ -188,23 +198,22 @@ struct SettingsContentView: View {
|
|
|
188
198
|
Image(systemName: section.icon)
|
|
189
199
|
.font(.system(size: 11, weight: .semibold))
|
|
190
200
|
.foregroundColor(active ? Palette.text : Palette.textMuted)
|
|
191
|
-
.frame(width: 16, alignment: .center)
|
|
201
|
+
.frame(width: 16, height: 18, alignment: .center)
|
|
192
202
|
|
|
193
203
|
VStack(alignment: .leading, spacing: 3) {
|
|
194
204
|
Text(section.title)
|
|
195
205
|
.font(Typo.mono(11))
|
|
196
206
|
.foregroundColor(active ? Palette.text : Palette.textMuted)
|
|
197
207
|
|
|
198
|
-
Text(section.
|
|
199
|
-
.font(Typo.caption(9
|
|
200
|
-
.foregroundColor(Palette.textMuted.opacity(active ? 0.
|
|
201
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
208
|
+
Text(section.eyebrow)
|
|
209
|
+
.font(Typo.caption(9))
|
|
210
|
+
.foregroundColor(Palette.textMuted.opacity(active ? 0.85 : 0.62))
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
Spacer(minLength: 0)
|
|
205
214
|
}
|
|
206
215
|
.padding(.horizontal, 10)
|
|
207
|
-
.padding(.vertical,
|
|
216
|
+
.padding(.vertical, 8)
|
|
208
217
|
.contentShape(RoundedRectangle(cornerRadius: 8))
|
|
209
218
|
.background(
|
|
210
219
|
ZStack {
|
|
@@ -254,14 +263,14 @@ struct SettingsContentView: View {
|
|
|
254
263
|
switch selectedTab {
|
|
255
264
|
case .general:
|
|
256
265
|
generalContent
|
|
257
|
-
case .
|
|
258
|
-
|
|
266
|
+
case .shortcuts:
|
|
267
|
+
shortcutsContent
|
|
259
268
|
case .ai:
|
|
260
269
|
aiContent
|
|
261
270
|
case .search:
|
|
262
271
|
searchOcrContent
|
|
263
|
-
case .
|
|
264
|
-
|
|
272
|
+
case .companion:
|
|
273
|
+
companionContent
|
|
265
274
|
}
|
|
266
275
|
}
|
|
267
276
|
|
|
@@ -286,24 +295,614 @@ struct SettingsContentView: View {
|
|
|
286
295
|
}
|
|
287
296
|
}
|
|
288
297
|
|
|
289
|
-
// MARK: - General
|
|
290
|
-
|
|
291
|
-
private var
|
|
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 {
|
|
292
801
|
ScrollView {
|
|
293
802
|
VStack(alignment: .leading, spacing: 12) {
|
|
803
|
+
permissionsAssistantCard
|
|
804
|
+
|
|
294
805
|
settingsCard {
|
|
295
806
|
VStack(alignment: .leading, spacing: 10) {
|
|
296
807
|
HStack(alignment: .center, spacing: 8) {
|
|
297
|
-
Image(systemName: "
|
|
808
|
+
Image(systemName: permChecker.allGranted ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
|
|
298
809
|
.font(.system(size: 11, weight: .medium))
|
|
299
|
-
.foregroundColor(Palette.running)
|
|
300
|
-
Text("
|
|
810
|
+
.foregroundColor(permChecker.allGranted ? Palette.running : Palette.detach)
|
|
811
|
+
Text("Permissions")
|
|
301
812
|
.font(Typo.mono(12))
|
|
302
813
|
.foregroundColor(Palette.text)
|
|
303
814
|
Spacer()
|
|
304
|
-
|
|
305
|
-
.
|
|
306
|
-
|
|
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
|
+
}
|
|
307
906
|
}
|
|
308
907
|
|
|
309
908
|
Text("Lattices can check for new signed releases and prepare the update here. You’ll confirm before the app quits and relaunches.")
|
|
@@ -364,7 +963,7 @@ struct SettingsContentView: View {
|
|
|
364
963
|
Button {
|
|
365
964
|
appUpdater.promptForUpdate()
|
|
366
965
|
} label: {
|
|
367
|
-
Text(appUpdater.isUpdating ? "Preparing
|
|
966
|
+
Text(appUpdater.isUpdating ? "Preparing..." : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
|
|
368
967
|
.font(Typo.monoBold(10))
|
|
369
968
|
.foregroundColor(Palette.text)
|
|
370
969
|
.padding(.horizontal, 12)
|
|
@@ -421,36 +1020,22 @@ struct SettingsContentView: View {
|
|
|
421
1020
|
}
|
|
422
1021
|
}
|
|
423
1022
|
}
|
|
1023
|
+
}
|
|
1024
|
+
.padding(16)
|
|
1025
|
+
.frame(maxWidth: 760, alignment: .leading)
|
|
1026
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
424
1029
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
Text("Terminal")
|
|
429
|
-
.font(Typo.mono(11))
|
|
430
|
-
.foregroundColor(Palette.text)
|
|
431
|
-
|
|
432
|
-
Picker("", selection: $prefs.terminal) {
|
|
433
|
-
ForEach(Terminal.installed) { t in
|
|
434
|
-
Text(t.rawValue).tag(t)
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
.pickerStyle(.segmented)
|
|
438
|
-
.labelsHidden()
|
|
439
|
-
|
|
440
|
-
Text("Used for attaching to tmux sessions")
|
|
441
|
-
.font(Typo.caption(10))
|
|
442
|
-
.foregroundColor(Palette.textMuted)
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ── tmux ──
|
|
1030
|
+
private var behaviorContent: some View {
|
|
1031
|
+
ScrollView {
|
|
1032
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
447
1033
|
settingsCard {
|
|
448
1034
|
VStack(alignment: .leading, spacing: 10) {
|
|
449
1035
|
Text("tmux")
|
|
450
1036
|
.font(Typo.mono(11))
|
|
451
1037
|
.foregroundColor(Palette.text)
|
|
452
1038
|
|
|
453
|
-
// Mode
|
|
454
1039
|
HStack {
|
|
455
1040
|
Text("Detach mode")
|
|
456
1041
|
.font(Typo.mono(10))
|
|
@@ -470,74 +1055,6 @@ struct SettingsContentView: View {
|
|
|
470
1055
|
: "Detaches sessions silently")
|
|
471
1056
|
.font(Typo.caption(9))
|
|
472
1057
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
473
|
-
|
|
474
|
-
cardDivider
|
|
475
|
-
|
|
476
|
-
// Project scan root
|
|
477
|
-
Text("Project scan root")
|
|
478
|
-
.font(Typo.mono(10))
|
|
479
|
-
.foregroundColor(Palette.textDim)
|
|
480
|
-
|
|
481
|
-
HStack(spacing: 6) {
|
|
482
|
-
TextField("~/dev", text: $prefs.scanRoot)
|
|
483
|
-
.textFieldStyle(.plain)
|
|
484
|
-
.font(Typo.mono(11))
|
|
485
|
-
.foregroundColor(Palette.text)
|
|
486
|
-
.padding(.horizontal, 8)
|
|
487
|
-
.padding(.vertical, 5)
|
|
488
|
-
.background(
|
|
489
|
-
RoundedRectangle(cornerRadius: 5)
|
|
490
|
-
.fill(Color.white.opacity(0.06))
|
|
491
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
Button {
|
|
495
|
-
let panel = NSOpenPanel()
|
|
496
|
-
panel.canChooseDirectories = true
|
|
497
|
-
panel.canChooseFiles = false
|
|
498
|
-
panel.allowsMultipleSelection = false
|
|
499
|
-
if !prefs.scanRoot.isEmpty {
|
|
500
|
-
panel.directoryURL = URL(fileURLWithPath: prefs.scanRoot)
|
|
501
|
-
}
|
|
502
|
-
if panel.runModal() == .OK, let url = panel.url {
|
|
503
|
-
prefs.scanRoot = url.path
|
|
504
|
-
}
|
|
505
|
-
} label: {
|
|
506
|
-
Image(systemName: "folder")
|
|
507
|
-
.font(.system(size: 11))
|
|
508
|
-
.foregroundColor(Palette.textDim)
|
|
509
|
-
.padding(6)
|
|
510
|
-
.background(
|
|
511
|
-
RoundedRectangle(cornerRadius: 5)
|
|
512
|
-
.fill(Color.white.opacity(0.06))
|
|
513
|
-
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
|
|
514
|
-
)
|
|
515
|
-
}
|
|
516
|
-
.buttonStyle(.plain)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
HStack {
|
|
520
|
-
Text("Scans for .lattices.json project configs")
|
|
521
|
-
.font(Typo.caption(9))
|
|
522
|
-
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
523
|
-
Spacer()
|
|
524
|
-
Button {
|
|
525
|
-
scanner.updateRoot(prefs.scanRoot)
|
|
526
|
-
scanner.scan()
|
|
527
|
-
} label: {
|
|
528
|
-
Text("Rescan")
|
|
529
|
-
.font(Typo.monoBold(10))
|
|
530
|
-
.foregroundColor(Palette.text)
|
|
531
|
-
.padding(.horizontal, 12)
|
|
532
|
-
.padding(.vertical, 4)
|
|
533
|
-
.background(
|
|
534
|
-
RoundedRectangle(cornerRadius: 4)
|
|
535
|
-
.fill(Palette.surfaceHov)
|
|
536
|
-
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
537
|
-
)
|
|
538
|
-
}
|
|
539
|
-
.buttonStyle(.plain)
|
|
540
|
-
}
|
|
541
1058
|
}
|
|
542
1059
|
}
|
|
543
1060
|
|
|
@@ -626,6 +1143,13 @@ struct SettingsContentView: View {
|
|
|
626
1143
|
}
|
|
627
1144
|
}
|
|
628
1145
|
|
|
1146
|
+
breakerStatusRow(
|
|
1147
|
+
state: mouseGestureController.breakerState,
|
|
1148
|
+
label: "Mouse gestures"
|
|
1149
|
+
) {
|
|
1150
|
+
mouseGestureController.reArmAfterBreakerTrip()
|
|
1151
|
+
}
|
|
1152
|
+
|
|
629
1153
|
HStack(spacing: 8) {
|
|
630
1154
|
Button {
|
|
631
1155
|
mouseShortcutStore.openConfiguration()
|
|
@@ -723,6 +1247,13 @@ struct SettingsContentView: View {
|
|
|
723
1247
|
}
|
|
724
1248
|
}
|
|
725
1249
|
|
|
1250
|
+
breakerStatusRow(
|
|
1251
|
+
state: keyboardRemapController.breakerState,
|
|
1252
|
+
label: "Keyboard remaps"
|
|
1253
|
+
) {
|
|
1254
|
+
keyboardRemapController.reArmAfterBreakerTrip()
|
|
1255
|
+
}
|
|
1256
|
+
|
|
726
1257
|
HStack(spacing: 8) {
|
|
727
1258
|
Button {
|
|
728
1259
|
keyboardRemapStore.openConfiguration()
|
|
@@ -772,35 +1303,7 @@ struct SettingsContentView: View {
|
|
|
772
1303
|
VStack(alignment: .leading, spacing: 12) {
|
|
773
1304
|
companionBridgeOverviewCard
|
|
774
1305
|
companionTrustedDevicesCard
|
|
775
|
-
|
|
776
|
-
settingsCard {
|
|
777
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
778
|
-
Text("Input")
|
|
779
|
-
.font(Typo.mono(11))
|
|
780
|
-
.foregroundColor(Palette.text)
|
|
781
|
-
|
|
782
|
-
HStack(alignment: .top, spacing: 12) {
|
|
783
|
-
VStack(alignment: .leading, spacing: 4) {
|
|
784
|
-
Text("Trackpad proxy")
|
|
785
|
-
.font(Typo.mono(10))
|
|
786
|
-
.foregroundColor(Palette.textDim)
|
|
787
|
-
Text("Allow paired companions with the input.trackpad grant to move the Mac pointer through the encrypted bridge.")
|
|
788
|
-
.font(Typo.caption(9.5))
|
|
789
|
-
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
790
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
Spacer()
|
|
794
|
-
|
|
795
|
-
Toggle("", isOn: $prefs.companionTrackpadEnabled)
|
|
796
|
-
.toggleStyle(.switch)
|
|
797
|
-
.controlSize(.small)
|
|
798
|
-
.labelsHidden()
|
|
799
|
-
.disabled(!prefs.companionBridgeEnabled)
|
|
800
|
-
.opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
1306
|
+
companionCockpitCard
|
|
804
1307
|
}
|
|
805
1308
|
.padding(16)
|
|
806
1309
|
.frame(maxWidth: 760, alignment: .leading)
|
|
@@ -1291,6 +1794,20 @@ struct SettingsContentView: View {
|
|
|
1291
1794
|
}
|
|
1292
1795
|
}
|
|
1293
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
|
+
|
|
1294
1811
|
// MARK: - Search & OCR
|
|
1295
1812
|
|
|
1296
1813
|
private func ocrNumField(_ value: Binding<Double>, width: CGFloat = 50) -> some View {
|
|
@@ -1688,6 +2205,149 @@ struct SettingsContentView: View {
|
|
|
1688
2205
|
.padding(.vertical, 3)
|
|
1689
2206
|
}
|
|
1690
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
|
+
|
|
1691
2351
|
// MARK: - Shortcuts
|
|
1692
2352
|
|
|
1693
2353
|
private var shortcutsContent: some View {
|
|
@@ -1707,9 +2367,8 @@ struct SettingsContentView: View {
|
|
|
1707
2367
|
ScrollView {
|
|
1708
2368
|
VStack(alignment: .leading, spacing: 0) {
|
|
1709
2369
|
VStack(alignment: .leading, spacing: 16) {
|
|
1710
|
-
companionCockpitCard
|
|
1711
|
-
|
|
1712
2370
|
shortcutsOverviewCard
|
|
2371
|
+
inputControlsCard
|
|
1713
2372
|
|
|
1714
2373
|
LazyVGrid(columns: sectionColumns, alignment: .leading, spacing: 16) {
|
|
1715
2374
|
shortcutsAppCard
|