@lattices/cli 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift}
RENAMED
|
@@ -22,8 +22,14 @@ private struct FocusRingSuppressor: ViewModifier {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
enum CommandModePresentation {
|
|
26
|
+
case panel
|
|
27
|
+
case embedded
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
struct CommandModeView: View {
|
|
26
31
|
@ObservedObject var state: CommandModeState
|
|
32
|
+
var presentation: CommandModePresentation = .panel
|
|
27
33
|
@State private var eventMonitor: Any?
|
|
28
34
|
@State private var mouseDownMonitor: Any?
|
|
29
35
|
@State private var mouseDragMonitor: Any?
|
|
@@ -36,41 +42,77 @@ struct CommandModeView: View {
|
|
|
36
42
|
state.phase == .desktopInventory
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
private var isEmbedded: Bool {
|
|
46
|
+
presentation == .embedded
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
// Column widths for inventory table
|
|
40
50
|
private static let sizeColW: CGFloat = 80
|
|
41
51
|
private static let tileColW: CGFloat = 60
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
var body: some View {
|
|
54
|
+
GeometryReader { geo in
|
|
55
|
+
let availableWidth = max(geo.size.width, 580)
|
|
56
|
+
let contentWidth = resolvedContentWidth(in: availableWidth)
|
|
57
|
+
|
|
58
|
+
Group {
|
|
59
|
+
if isEmbedded && isDesktopInventory {
|
|
60
|
+
embeddedInventoryPage(contentWidth: contentWidth)
|
|
61
|
+
} else {
|
|
62
|
+
inventoryCard(contentWidth: contentWidth)
|
|
63
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
67
|
+
}
|
|
68
|
+
.onAppear { installKeyHandler(); installMouseMonitors() }
|
|
69
|
+
.onDisappear { removeKeyHandler(); removeMouseMonitors() }
|
|
70
|
+
.onChange(of: state.desktopMode) { mode in
|
|
71
|
+
CommandModeWindow.shared.panelWindow?.isMovableByWindowBackground = true
|
|
72
|
+
}
|
|
73
|
+
.animation(.easeInOut(duration: 0.2), value: isDesktopInventory)
|
|
74
|
+
.modifier(FocusRingSuppressor())
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
private
|
|
77
|
+
private func resolvedContentWidth(in availableWidth: CGFloat) -> CGFloat {
|
|
50
78
|
if isDesktopInventory {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
if isEmbedded {
|
|
80
|
+
return min(max(availableWidth - 24, 840), 1560)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let displayCount = CGFloat(max(1, state.filteredSnapshot?.displays.count ?? 1))
|
|
84
|
+
let ideal = displayCount * 480 + CGFloat(max(0, Int(displayCount) - 1)) + 32
|
|
85
|
+
let screenWidth = NSScreen.main?.visibleFrame.width ?? availableWidth
|
|
54
86
|
return min(ideal, screenWidth * 0.92)
|
|
55
87
|
}
|
|
88
|
+
|
|
89
|
+
if isEmbedded {
|
|
90
|
+
return min(720, max(availableWidth - 32, 580))
|
|
91
|
+
}
|
|
56
92
|
return 580
|
|
57
93
|
}
|
|
58
94
|
|
|
59
|
-
|
|
95
|
+
private func displayColumnWidth(for contentWidth: CGFloat) -> CGFloat {
|
|
96
|
+
let count = CGFloat(max(1, state.filteredSnapshot?.displays.count ?? 1))
|
|
97
|
+
let available = contentWidth - 32 - (count - 1) * 0.5
|
|
98
|
+
return max(isEmbedded ? 400 : 360, (available / count).rounded(.down))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func inventoryCard(contentWidth: CGFloat) -> some View {
|
|
60
102
|
VStack(spacing: 0) {
|
|
61
103
|
header
|
|
62
104
|
divider
|
|
63
105
|
if isDesktopInventory && state.desktopMode == .gridPreview {
|
|
64
106
|
gridPreviewContent
|
|
65
107
|
} else if isDesktopInventory {
|
|
66
|
-
desktopInventoryContent
|
|
108
|
+
desktopInventoryContent(contentWidth: contentWidth)
|
|
67
109
|
} else {
|
|
68
110
|
inventoryGrid
|
|
69
111
|
}
|
|
70
112
|
divider
|
|
71
113
|
chordFooter
|
|
72
114
|
}
|
|
73
|
-
.frame(width:
|
|
115
|
+
.frame(width: contentWidth)
|
|
74
116
|
.background(Palette.bg)
|
|
75
117
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
76
118
|
.overlay(
|
|
@@ -79,13 +121,79 @@ struct CommandModeView: View {
|
|
|
79
121
|
)
|
|
80
122
|
.overlay(executingOverlay)
|
|
81
123
|
.overlay(flashOverlay)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func embeddedInventoryPage(contentWidth: CGFloat) -> some View {
|
|
127
|
+
VStack(spacing: 12) {
|
|
128
|
+
embeddedInventorySummary
|
|
129
|
+
.frame(width: contentWidth, alignment: .leading)
|
|
130
|
+
inventoryCard(contentWidth: contentWidth)
|
|
131
|
+
.frame(maxHeight: .infinity)
|
|
86
132
|
}
|
|
87
|
-
.
|
|
88
|
-
.
|
|
133
|
+
.padding(16)
|
|
134
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private var embeddedInventorySummary: some View {
|
|
138
|
+
let snapshot = state.filteredSnapshot ?? state.desktopSnapshot
|
|
139
|
+
let displayCount = snapshot?.displays.count ?? 0
|
|
140
|
+
let spaceCount = snapshot?.displays.reduce(0) { total, display in
|
|
141
|
+
total + display.spaces.count
|
|
142
|
+
} ?? 0
|
|
143
|
+
let windowCount = snapshot?.allWindows.count ?? 0
|
|
144
|
+
|
|
145
|
+
return HStack(alignment: .center, spacing: 12) {
|
|
146
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
147
|
+
Text("Grouped by display, Space, and app")
|
|
148
|
+
.font(Typo.heading(13))
|
|
149
|
+
.foregroundColor(Palette.text)
|
|
150
|
+
Text(state.isSearching
|
|
151
|
+
? "Search results stay in place so the desktop reads like a map instead of a flat list."
|
|
152
|
+
: "Live window sizes, tiling hints, and OCR search in one balanced pass across the desktop.")
|
|
153
|
+
.font(Typo.mono(10))
|
|
154
|
+
.foregroundColor(Palette.textDim)
|
|
155
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Spacer(minLength: 12)
|
|
159
|
+
|
|
160
|
+
HStack(spacing: 8) {
|
|
161
|
+
inventoryStatPill(value: displayCount, label: "Displays")
|
|
162
|
+
inventoryStatPill(value: spaceCount, label: "Spaces")
|
|
163
|
+
inventoryStatPill(value: windowCount, label: "Windows")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
.padding(.horizontal, 14)
|
|
167
|
+
.padding(.vertical, 12)
|
|
168
|
+
.background(
|
|
169
|
+
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
170
|
+
.fill(Palette.surface.opacity(0.65))
|
|
171
|
+
.overlay(
|
|
172
|
+
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
173
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func inventoryStatPill(value: Int, label: String) -> some View {
|
|
179
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
180
|
+
Text("\(value)")
|
|
181
|
+
.font(Typo.monoBold(12))
|
|
182
|
+
.foregroundColor(Palette.text)
|
|
183
|
+
Text(label.uppercased())
|
|
184
|
+
.font(Typo.mono(8))
|
|
185
|
+
.foregroundColor(Palette.textMuted)
|
|
186
|
+
}
|
|
187
|
+
.padding(.horizontal, 10)
|
|
188
|
+
.padding(.vertical, 8)
|
|
189
|
+
.background(
|
|
190
|
+
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
191
|
+
.fill(Palette.bg.opacity(0.75))
|
|
192
|
+
.overlay(
|
|
193
|
+
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
194
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
195
|
+
)
|
|
196
|
+
)
|
|
89
197
|
}
|
|
90
198
|
|
|
91
199
|
// MARK: - Header
|
|
@@ -121,23 +229,6 @@ struct CommandModeView: View {
|
|
|
121
229
|
|
|
122
230
|
Spacer()
|
|
123
231
|
|
|
124
|
-
if let layer = state.inventory.activeLayer {
|
|
125
|
-
HStack(spacing: 4) {
|
|
126
|
-
Text("Layer: \(layer)")
|
|
127
|
-
.font(Typo.mono(10))
|
|
128
|
-
.foregroundColor(Palette.running)
|
|
129
|
-
|
|
130
|
-
Text("[\(state.inventory.layerCount > 0 ? "\(WorkspaceManager.shared.activeLayerIndex + 1)/\(state.inventory.layerCount)" : "—")]")
|
|
131
|
-
.font(Typo.mono(10))
|
|
132
|
-
.foregroundColor(Palette.textMuted)
|
|
133
|
-
}
|
|
134
|
-
.padding(.horizontal, 6)
|
|
135
|
-
.padding(.vertical, 2)
|
|
136
|
-
.background(
|
|
137
|
-
RoundedRectangle(cornerRadius: 3)
|
|
138
|
-
.fill(Palette.running.opacity(0.10))
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
232
|
}
|
|
142
233
|
.padding(.horizontal, 16)
|
|
143
234
|
.padding(.vertical, 10)
|
|
@@ -155,15 +246,12 @@ struct CommandModeView: View {
|
|
|
155
246
|
private var inventoryGrid: some View {
|
|
156
247
|
ScrollView {
|
|
157
248
|
LazyVStack(alignment: .leading, spacing: 0) {
|
|
158
|
-
let
|
|
159
|
-
if
|
|
249
|
+
let items = state.inventory.items
|
|
250
|
+
if items.isEmpty {
|
|
160
251
|
emptyState
|
|
161
252
|
} else {
|
|
162
|
-
ForEach(
|
|
163
|
-
|
|
164
|
-
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
165
|
-
inventoryRow(item)
|
|
166
|
-
}
|
|
253
|
+
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
254
|
+
inventoryRow(item)
|
|
167
255
|
}
|
|
168
256
|
}
|
|
169
257
|
}
|
|
@@ -185,7 +273,7 @@ struct CommandModeView: View {
|
|
|
185
273
|
|
|
186
274
|
// MARK: - Desktop Inventory Content
|
|
187
275
|
|
|
188
|
-
private
|
|
276
|
+
private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
|
|
189
277
|
VStack(spacing: 0) {
|
|
190
278
|
if state.isSearching {
|
|
191
279
|
searchBar
|
|
@@ -197,20 +285,7 @@ struct CommandModeView: View {
|
|
|
197
285
|
ZStack {
|
|
198
286
|
Group {
|
|
199
287
|
if let snapshot = state.filteredSnapshot, !snapshot.displays.isEmpty {
|
|
200
|
-
|
|
201
|
-
HStack(alignment: .top, spacing: 0) {
|
|
202
|
-
let total = snapshot.displays.count
|
|
203
|
-
ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
|
|
204
|
-
if idx > 0 {
|
|
205
|
-
Rectangle()
|
|
206
|
-
.fill(Palette.border)
|
|
207
|
-
.frame(width: 0.5)
|
|
208
|
-
}
|
|
209
|
-
displayColumn(display, index: idx, total: total)
|
|
210
|
-
.frame(width: displayColumnWidth)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
288
|
+
inventoryColumns(snapshot: snapshot, contentWidth: contentWidth)
|
|
214
289
|
} else {
|
|
215
290
|
desktopEmptyState
|
|
216
291
|
}
|
|
@@ -236,6 +311,53 @@ struct CommandModeView: View {
|
|
|
236
311
|
}
|
|
237
312
|
}
|
|
238
313
|
|
|
314
|
+
@ViewBuilder
|
|
315
|
+
private func inventoryColumns(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> some View {
|
|
316
|
+
if shouldShowEmbeddedSidebar(snapshot: snapshot, contentWidth: contentWidth),
|
|
317
|
+
let display = snapshot.displays.first {
|
|
318
|
+
embeddedSingleDisplayLayout(display: display, contentWidth: contentWidth)
|
|
319
|
+
} else {
|
|
320
|
+
ScrollView(.horizontal, showsIndicators: false) {
|
|
321
|
+
HStack(alignment: .top, spacing: 0) {
|
|
322
|
+
let total = snapshot.displays.count
|
|
323
|
+
ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
|
|
324
|
+
if idx > 0 {
|
|
325
|
+
Rectangle()
|
|
326
|
+
.fill(Palette.border)
|
|
327
|
+
.frame(width: 0.5)
|
|
328
|
+
}
|
|
329
|
+
displayColumn(display, index: idx, total: total)
|
|
330
|
+
.frame(width: displayColumnWidth(for: contentWidth))
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private func shouldShowEmbeddedSidebar(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> Bool {
|
|
338
|
+
isEmbedded && snapshot.displays.count == 1 && contentWidth >= 900
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func embeddedSingleDisplayLayout(
|
|
342
|
+
display: DesktopInventorySnapshot.DisplayInfo,
|
|
343
|
+
contentWidth: CGFloat
|
|
344
|
+
) -> some View {
|
|
345
|
+
let sidebarWidth = min(max(contentWidth * 0.28, 250), 330)
|
|
346
|
+
let mainWidth = max(contentWidth - sidebarWidth - 0.5, 620)
|
|
347
|
+
|
|
348
|
+
return HStack(alignment: .top, spacing: 0) {
|
|
349
|
+
displayColumn(display, index: 0, total: 1)
|
|
350
|
+
.frame(width: mainWidth)
|
|
351
|
+
|
|
352
|
+
Rectangle()
|
|
353
|
+
.fill(Palette.border)
|
|
354
|
+
.frame(width: 0.5)
|
|
355
|
+
|
|
356
|
+
embeddedInventorySidebar(display: display)
|
|
357
|
+
.frame(width: sidebarWidth)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
239
361
|
private var filterPillBar: some View {
|
|
240
362
|
HStack(spacing: 6) {
|
|
241
363
|
ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
|
|
@@ -398,6 +520,210 @@ struct CommandModeView: View {
|
|
|
398
520
|
.padding(.vertical, 8)
|
|
399
521
|
}
|
|
400
522
|
|
|
523
|
+
private func embeddedInventorySidebar(display: DesktopInventorySnapshot.DisplayInfo) -> some View {
|
|
524
|
+
ScrollView {
|
|
525
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
526
|
+
sidebarCard(title: "Overview") {
|
|
527
|
+
sidebarMetric(label: "Display", value: display.name)
|
|
528
|
+
sidebarMetric(
|
|
529
|
+
label: "Visible",
|
|
530
|
+
value: "\(display.visibleFrame.w)×\(display.visibleFrame.h)"
|
|
531
|
+
)
|
|
532
|
+
sidebarMetric(
|
|
533
|
+
label: "Current Space",
|
|
534
|
+
value: "Space \(display.currentSpaceIndex)"
|
|
535
|
+
)
|
|
536
|
+
sidebarMetric(
|
|
537
|
+
label: "Windows",
|
|
538
|
+
value: "\(windowCount(in: display)) total"
|
|
539
|
+
)
|
|
540
|
+
sidebarMetric(
|
|
541
|
+
label: "Apps",
|
|
542
|
+
value: "\(uniqueAppCount(in: display)) active"
|
|
543
|
+
)
|
|
544
|
+
sidebarMetric(
|
|
545
|
+
label: "Lattices",
|
|
546
|
+
value: "\(latticesWindowCount(in: display)) tagged"
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
sidebarCard(title: "Spaces") {
|
|
551
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
552
|
+
ForEach(display.spaces) { space in
|
|
553
|
+
HStack(alignment: .top, spacing: 8) {
|
|
554
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
555
|
+
HStack(spacing: 5) {
|
|
556
|
+
Text("Space \(space.index)")
|
|
557
|
+
.font(Typo.monoBold(10))
|
|
558
|
+
.foregroundColor(space.isCurrent ? Palette.running : Palette.text)
|
|
559
|
+
if space.isCurrent {
|
|
560
|
+
Text("active")
|
|
561
|
+
.font(Typo.mono(8))
|
|
562
|
+
.foregroundColor(Palette.running.opacity(0.75))
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
Text("\(spaceWindowCount(space)) windows across \(space.apps.count) apps")
|
|
566
|
+
.font(Typo.mono(9))
|
|
567
|
+
.foregroundColor(Palette.textDim)
|
|
568
|
+
}
|
|
569
|
+
Spacer()
|
|
570
|
+
Text("\(spaceLatticesCount(space))")
|
|
571
|
+
.font(Typo.mono(9))
|
|
572
|
+
.foregroundColor(Palette.textMuted)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
sidebarCard(title: state.selectedWindowIds.isEmpty ? "Top Apps" : "Selection") {
|
|
579
|
+
if state.selectedWindowIds.isEmpty {
|
|
580
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
581
|
+
Text("Select a window to inspect it here.")
|
|
582
|
+
.font(Typo.mono(9))
|
|
583
|
+
.foregroundColor(Palette.textDim)
|
|
584
|
+
|
|
585
|
+
ForEach(topApps(in: display), id: \.name) { app in
|
|
586
|
+
HStack(spacing: 8) {
|
|
587
|
+
Text(app.name)
|
|
588
|
+
.font(Typo.monoBold(10))
|
|
589
|
+
.foregroundColor(Palette.text)
|
|
590
|
+
.lineLimit(1)
|
|
591
|
+
Spacer()
|
|
592
|
+
Text("\(app.count)")
|
|
593
|
+
.font(Typo.mono(9))
|
|
594
|
+
.foregroundColor(Palette.textMuted)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
selectionSidebarContent
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
sidebarCard(title: "Keys") {
|
|
604
|
+
VStack(alignment: .leading, spacing: 7) {
|
|
605
|
+
sidebarShortcut("Arrows", "move through windows")
|
|
606
|
+
sidebarShortcut("/", "search by title or OCR")
|
|
607
|
+
sidebarShortcut("M", "jump to Screen Map")
|
|
608
|
+
sidebarShortcut("T", "tile selected window")
|
|
609
|
+
sidebarShortcut("Esc", "back or clear selection")
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
.padding(12)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@ViewBuilder
|
|
618
|
+
private var selectionSidebarContent: some View {
|
|
619
|
+
let selected = selectedWindows
|
|
620
|
+
|
|
621
|
+
if selected.count > 1 {
|
|
622
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
623
|
+
Text("\(selected.count) windows selected")
|
|
624
|
+
.font(Typo.monoBold(10))
|
|
625
|
+
.foregroundColor(Palette.text)
|
|
626
|
+
if !state.selectedWindowSummaryText.isEmpty {
|
|
627
|
+
Text(state.selectedWindowSummaryText)
|
|
628
|
+
.font(Typo.mono(9))
|
|
629
|
+
.foregroundColor(Palette.textDim)
|
|
630
|
+
}
|
|
631
|
+
ForEach(Array(selected.prefix(5)), id: \.id) { window in
|
|
632
|
+
HStack(spacing: 8) {
|
|
633
|
+
Text(window.appName ?? "Unknown")
|
|
634
|
+
.font(Typo.monoBold(9))
|
|
635
|
+
.foregroundColor(window.isLattices ? Palette.running : Palette.text)
|
|
636
|
+
.lineLimit(1)
|
|
637
|
+
Spacer()
|
|
638
|
+
Text(sizeText(window.frame))
|
|
639
|
+
.font(Typo.mono(9))
|
|
640
|
+
.foregroundColor(Palette.textMuted)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} else if let window = selected.first {
|
|
645
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
646
|
+
Text(window.appName ?? "Unknown")
|
|
647
|
+
.font(Typo.monoBold(10))
|
|
648
|
+
.foregroundColor(window.isLattices ? Palette.running : Palette.text)
|
|
649
|
+
Text(window.title.isEmpty ? "(untitled)" : window.title)
|
|
650
|
+
.font(Typo.mono(9))
|
|
651
|
+
.foregroundColor(Palette.textDim)
|
|
652
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
653
|
+
sidebarMetric(label: "Size", value: sizeText(window.frame))
|
|
654
|
+
if let tile = window.tilePosition?.label {
|
|
655
|
+
sidebarMetric(label: "Tile", value: tile)
|
|
656
|
+
}
|
|
657
|
+
if let session = window.latticesSession {
|
|
658
|
+
sidebarMetric(label: "Session", value: session)
|
|
659
|
+
}
|
|
660
|
+
if let path = window.inventoryPath {
|
|
661
|
+
Text(path.description)
|
|
662
|
+
.font(Typo.mono(8))
|
|
663
|
+
.foregroundColor(Palette.textMuted)
|
|
664
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private func sidebarCard<Content: View>(
|
|
671
|
+
title: String,
|
|
672
|
+
@ViewBuilder content: () -> Content
|
|
673
|
+
) -> some View {
|
|
674
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
675
|
+
Text(title.uppercased())
|
|
676
|
+
.font(Typo.mono(9))
|
|
677
|
+
.foregroundColor(Palette.textMuted)
|
|
678
|
+
content()
|
|
679
|
+
}
|
|
680
|
+
.padding(12)
|
|
681
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
682
|
+
.background(
|
|
683
|
+
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
684
|
+
.fill(Palette.surface.opacity(0.55))
|
|
685
|
+
.overlay(
|
|
686
|
+
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
687
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private func sidebarMetric(label: String, value: String) -> some View {
|
|
693
|
+
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
|
694
|
+
Text(label.uppercased())
|
|
695
|
+
.font(Typo.mono(8))
|
|
696
|
+
.foregroundColor(Palette.textMuted)
|
|
697
|
+
.frame(width: 74, alignment: .leading)
|
|
698
|
+
Text(value)
|
|
699
|
+
.font(Typo.mono(9))
|
|
700
|
+
.foregroundColor(Palette.text)
|
|
701
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
702
|
+
Spacer(minLength: 0)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private func sidebarShortcut(_ key: String, _ label: String) -> some View {
|
|
707
|
+
HStack(spacing: 8) {
|
|
708
|
+
Text(key)
|
|
709
|
+
.font(Typo.monoBold(9))
|
|
710
|
+
.foregroundColor(Palette.text)
|
|
711
|
+
.padding(.horizontal, 6)
|
|
712
|
+
.padding(.vertical, 3)
|
|
713
|
+
.background(
|
|
714
|
+
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
|
715
|
+
.fill(Palette.bg.opacity(0.8))
|
|
716
|
+
.overlay(
|
|
717
|
+
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
|
718
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
Text(label)
|
|
722
|
+
.font(Typo.mono(9))
|
|
723
|
+
.foregroundColor(Palette.textDim)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
401
727
|
private func spaceHeader(_ space: DesktopInventorySnapshot.SpaceGroup, display: DesktopInventorySnapshot.DisplayInfo) -> some View {
|
|
402
728
|
HStack(spacing: 5) {
|
|
403
729
|
Text("Space \(space.index)")
|
|
@@ -630,15 +956,7 @@ struct CommandModeView: View {
|
|
|
630
956
|
Menu("Tile All (\(selCount))") {
|
|
631
957
|
ForEach(TilePosition.allCases) { tile in
|
|
632
958
|
Button {
|
|
633
|
-
|
|
634
|
-
for (i, win) in windows.enumerated() {
|
|
635
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.1) {
|
|
636
|
-
WindowTiler.tileWindowById(wid: win.id, pid: win.pid, to: tile)
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3 + Double(windows.count) * 0.1) {
|
|
640
|
-
state.desktopSnapshot = nil
|
|
641
|
-
}
|
|
959
|
+
state.showAndDistributeSelected(in: .tile(tile))
|
|
642
960
|
} label: {
|
|
643
961
|
Label(tile.label, systemImage: tile.icon)
|
|
644
962
|
}
|
|
@@ -703,9 +1021,6 @@ struct CommandModeView: View {
|
|
|
703
1021
|
private func windowTitle(_ window: DesktopInventorySnapshot.InventoryWindowInfo) -> String {
|
|
704
1022
|
let title = window.title
|
|
705
1023
|
if title.isEmpty { return "(untitled)" }
|
|
706
|
-
if title.count > 30 {
|
|
707
|
-
return String(title.prefix(27)) + "..."
|
|
708
|
-
}
|
|
709
1024
|
return title
|
|
710
1025
|
}
|
|
711
1026
|
|
|
@@ -713,6 +1028,57 @@ struct CommandModeView: View {
|
|
|
713
1028
|
"\(Int(frame.w))×\(Int(frame.h))"
|
|
714
1029
|
}
|
|
715
1030
|
|
|
1031
|
+
private var selectedWindows: [DesktopInventorySnapshot.InventoryWindowInfo] {
|
|
1032
|
+
state.flatWindowList.filter { state.selectedWindowIds.contains($0.id) }
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private func windowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
|
|
1036
|
+
display.spaces.reduce(0) { total, space in
|
|
1037
|
+
total + spaceWindowCount(space)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private func uniqueAppCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
|
|
1042
|
+
Set(display.spaces.flatMap { $0.apps.map(\.appName) }).count
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private func latticesWindowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
|
|
1046
|
+
display.spaces.reduce(0) { total, space in
|
|
1047
|
+
total + spaceLatticesCount(space)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private func spaceWindowCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
|
|
1052
|
+
space.apps.reduce(0) { total, app in
|
|
1053
|
+
total + app.windows.count
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private func spaceLatticesCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
|
|
1058
|
+
space.apps.reduce(0) { total, app in
|
|
1059
|
+
total + app.windows.filter(\.isLattices).count
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
private func topApps(
|
|
1064
|
+
in display: DesktopInventorySnapshot.DisplayInfo
|
|
1065
|
+
) -> [(name: String, count: Int)] {
|
|
1066
|
+
var counts: [String: Int] = [:]
|
|
1067
|
+
for space in display.spaces {
|
|
1068
|
+
for app in space.apps {
|
|
1069
|
+
counts[app.appName, default: 0] += app.windows.count
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return counts
|
|
1073
|
+
.map { (name: $0.key, count: $0.value) }
|
|
1074
|
+
.sorted { lhs, rhs in
|
|
1075
|
+
if lhs.count == rhs.count { return lhs.name < rhs.name }
|
|
1076
|
+
return lhs.count > rhs.count
|
|
1077
|
+
}
|
|
1078
|
+
.prefix(5)
|
|
1079
|
+
.map { $0 }
|
|
1080
|
+
}
|
|
1081
|
+
|
|
716
1082
|
/// Group items by their group label
|
|
717
1083
|
private var groupedItems: [(String, [CommandModeInventory.Item])] {
|
|
718
1084
|
var result: [(String, [CommandModeInventory.Item])] = []
|
|
@@ -855,10 +1221,16 @@ struct CommandModeView: View {
|
|
|
855
1221
|
if isDesktopInventory && state.desktopMode == .gridPreview {
|
|
856
1222
|
// Grid preview hints
|
|
857
1223
|
HStack(spacing: 12) {
|
|
1224
|
+
chordHint(key: "←→↑↓", label: "region")
|
|
1225
|
+
chordHint(key: "1-7", label: "corners/thirds")
|
|
1226
|
+
chordHint(key: "c", label: "center")
|
|
858
1227
|
chordHint(key: "↩", label: "apply layout")
|
|
859
1228
|
chordHint(key: "s", label: "apply layout")
|
|
860
1229
|
chordHint(key: "esc", label: "cancel")
|
|
861
1230
|
Spacer()
|
|
1231
|
+
Text(state.gridPreviewRegionLabel.uppercased())
|
|
1232
|
+
.font(Typo.mono(9))
|
|
1233
|
+
.foregroundColor(Palette.textDim)
|
|
862
1234
|
let shape = state.gridPreviewShape
|
|
863
1235
|
Text(shape.map(String.init).joined(separator: " + "))
|
|
864
1236
|
.font(Typo.monoBold(9))
|
|
@@ -910,13 +1282,19 @@ struct CommandModeView: View {
|
|
|
910
1282
|
} else if isDesktopInventory && state.selectedWindowIds.count > 1 {
|
|
911
1283
|
// Multi-selection active
|
|
912
1284
|
HStack(spacing: 12) {
|
|
913
|
-
chordHint(key: "s", label: "
|
|
1285
|
+
chordHint(key: "s", label: "grid preview")
|
|
914
1286
|
chordHint(key: "↩", label: "front")
|
|
915
|
-
chordHint(key: "t", label: "
|
|
1287
|
+
chordHint(key: "t", label: "grid region")
|
|
916
1288
|
chordHint(key: "f", label: "focus")
|
|
917
1289
|
chordHint(key: "h", label: "highlight")
|
|
918
1290
|
chordHint(key: "esc", label: "clear")
|
|
919
1291
|
Spacer()
|
|
1292
|
+
if !state.selectedWindowSummaryText.isEmpty {
|
|
1293
|
+
Text(state.selectedWindowSummaryText)
|
|
1294
|
+
.font(Typo.mono(9))
|
|
1295
|
+
.foregroundColor(Palette.textDim)
|
|
1296
|
+
.lineLimit(1)
|
|
1297
|
+
}
|
|
920
1298
|
Text("\(state.selectedWindowIds.count) selected")
|
|
921
1299
|
.font(Typo.mono(9))
|
|
922
1300
|
.foregroundColor(Palette.running)
|
|
@@ -1102,6 +1480,9 @@ struct CommandModeView: View {
|
|
|
1102
1480
|
Text("LAYOUT PREVIEW")
|
|
1103
1481
|
.font(Typo.monoBold(10))
|
|
1104
1482
|
.foregroundColor(Palette.textDim)
|
|
1483
|
+
Text(state.gridPreviewRegionLabel.uppercased())
|
|
1484
|
+
.font(Typo.mono(9))
|
|
1485
|
+
.foregroundColor(Palette.textMuted)
|
|
1105
1486
|
Text(gridDesc)
|
|
1106
1487
|
.font(Typo.monoBold(10))
|
|
1107
1488
|
.foregroundColor(Palette.running)
|
|
@@ -1116,7 +1497,7 @@ struct CommandModeView: View {
|
|
|
1116
1497
|
divider
|
|
1117
1498
|
|
|
1118
1499
|
// Screen map: current positions (dimmed) + target grid (bright)
|
|
1119
|
-
screenMap(windows: windows, shape: shape)
|
|
1500
|
+
screenMap(windows: windows, shape: shape, placement: state.gridPreviewPlacement)
|
|
1120
1501
|
.frame(height: 160)
|
|
1121
1502
|
.padding(.horizontal, 12)
|
|
1122
1503
|
.padding(.vertical, 8)
|
|
@@ -1145,7 +1526,11 @@ struct CommandModeView: View {
|
|
|
1145
1526
|
// MARK: - Grid Preview Screen Map
|
|
1146
1527
|
|
|
1147
1528
|
/// Miniature proportional map of the screen showing current window positions and target grid slots
|
|
1148
|
-
private func screenMap(
|
|
1529
|
+
private func screenMap(
|
|
1530
|
+
windows: [DesktopInventorySnapshot.InventoryWindowInfo],
|
|
1531
|
+
shape: [Int],
|
|
1532
|
+
placement: PlacementSpec?
|
|
1533
|
+
) -> some View {
|
|
1149
1534
|
GeometryReader { geo in
|
|
1150
1535
|
let availW = geo.size.width
|
|
1151
1536
|
let availH = geo.size.height
|
|
@@ -1174,6 +1559,14 @@ struct CommandModeView: View {
|
|
|
1174
1559
|
)
|
|
1175
1560
|
.frame(width: mapW, height: mapH)
|
|
1176
1561
|
|
|
1562
|
+
if let placement {
|
|
1563
|
+
let region = placement.fractions
|
|
1564
|
+
RoundedRectangle(cornerRadius: 4)
|
|
1565
|
+
.strokeBorder(Palette.running.opacity(0.35), style: StrokeStyle(lineWidth: 1, dash: [6, 4]))
|
|
1566
|
+
.frame(width: mapW * region.2, height: mapH * region.3)
|
|
1567
|
+
.offset(x: mapW * region.0, y: mapH * region.1)
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1177
1570
|
// Current positions (dimmed)
|
|
1178
1571
|
ForEach(Array(windows.enumerated()), id: \.element.id) { idx, win in
|
|
1179
1572
|
let f = win.frame
|
|
@@ -1193,7 +1586,13 @@ struct CommandModeView: View {
|
|
|
1193
1586
|
}
|
|
1194
1587
|
|
|
1195
1588
|
// Target grid slots (bright)
|
|
1196
|
-
let slots = computeMapSlots(
|
|
1589
|
+
let slots = computeMapSlots(
|
|
1590
|
+
count: windows.count,
|
|
1591
|
+
shape: shape,
|
|
1592
|
+
mapW: mapW,
|
|
1593
|
+
mapH: mapH,
|
|
1594
|
+
region: placement?.fractions
|
|
1595
|
+
)
|
|
1197
1596
|
ForEach(Array(slots.enumerated()), id: \.offset) { idx, slot in
|
|
1198
1597
|
let win = idx < windows.count ? windows[idx] : nil
|
|
1199
1598
|
RoundedRectangle(cornerRadius: 2)
|
|
@@ -1224,15 +1623,30 @@ struct CommandModeView: View {
|
|
|
1224
1623
|
}
|
|
1225
1624
|
|
|
1226
1625
|
/// Compute grid slots scaled to the mini map dimensions
|
|
1227
|
-
private func computeMapSlots(
|
|
1626
|
+
private func computeMapSlots(
|
|
1627
|
+
count: Int,
|
|
1628
|
+
shape: [Int],
|
|
1629
|
+
mapW: CGFloat,
|
|
1630
|
+
mapH: CGFloat,
|
|
1631
|
+
region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil
|
|
1632
|
+
) -> [CGRect] {
|
|
1633
|
+
let regionX = mapW * (region?.0 ?? 0)
|
|
1634
|
+
let regionY = mapH * (region?.1 ?? 0)
|
|
1635
|
+
let regionW = mapW * (region?.2 ?? 1)
|
|
1636
|
+
let regionH = mapH * (region?.3 ?? 1)
|
|
1228
1637
|
let rowCount = shape.count
|
|
1229
|
-
let rowH =
|
|
1638
|
+
let rowH = regionH / CGFloat(rowCount)
|
|
1230
1639
|
var slots: [CGRect] = []
|
|
1231
1640
|
for (row, cols) in shape.enumerated() {
|
|
1232
|
-
let colW =
|
|
1233
|
-
let y = CGFloat(row) * rowH
|
|
1641
|
+
let colW = regionW / CGFloat(cols)
|
|
1642
|
+
let y = regionY + CGFloat(row) * rowH
|
|
1234
1643
|
for col in 0..<cols {
|
|
1235
|
-
slots.append(CGRect(
|
|
1644
|
+
slots.append(CGRect(
|
|
1645
|
+
x: regionX + CGFloat(col) * colW,
|
|
1646
|
+
y: y,
|
|
1647
|
+
width: colW,
|
|
1648
|
+
height: rowH
|
|
1649
|
+
))
|
|
1236
1650
|
}
|
|
1237
1651
|
}
|
|
1238
1652
|
return slots
|
|
@@ -1402,4 +1816,3 @@ struct CommandModeView: View {
|
|
|
1402
1816
|
}
|
|
1403
1817
|
}
|
|
1404
1818
|
}
|
|
1405
|
-
|