@lattices/cli 0.4.14 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/apps/mac/Info.plist +2 -2
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +4 -5
- package/docs/voice.md +11 -27
- package/package.json +9 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,1927 +0,0 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
|
-
import AppKit
|
|
3
|
-
|
|
4
|
-
// MARK: - Row Frame PreferenceKey
|
|
5
|
-
|
|
6
|
-
struct WindowRowFrameKey: PreferenceKey {
|
|
7
|
-
static var defaultValue: [UInt32: CGRect] = [:]
|
|
8
|
-
static func reduce(value: inout [UInt32: CGRect], nextValue: () -> [UInt32: CGRect]) {
|
|
9
|
-
value.merge(nextValue(), uniquingKeysWith: { _, new in new })
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// MARK: - Focus Ring Suppressor
|
|
14
|
-
|
|
15
|
-
private struct FocusRingSuppressor: ViewModifier {
|
|
16
|
-
func body(content: Content) -> some View {
|
|
17
|
-
if #available(macOS 14, *) {
|
|
18
|
-
content.focusEffectDisabled()
|
|
19
|
-
} else {
|
|
20
|
-
content
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
enum CommandModePresentation {
|
|
26
|
-
case panel
|
|
27
|
-
case embedded
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
struct CommandModeView: View {
|
|
31
|
-
@ObservedObject var state: CommandModeState
|
|
32
|
-
var presentation: CommandModePresentation = .panel
|
|
33
|
-
@State private var eventMonitor: Any?
|
|
34
|
-
@State private var mouseDownMonitor: Any?
|
|
35
|
-
@State private var mouseDragMonitor: Any?
|
|
36
|
-
@State private var mouseUpMonitor: Any?
|
|
37
|
-
@State private var panelOriginY: CGFloat = 0
|
|
38
|
-
@State private var hoveredWindowId: UInt32?
|
|
39
|
-
@FocusState private var isSearchFieldFocused: Bool
|
|
40
|
-
|
|
41
|
-
private var isDesktopInventory: Bool {
|
|
42
|
-
state.phase == .desktopInventory
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private var isEmbedded: Bool {
|
|
46
|
-
presentation == .embedded
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Column widths for inventory table
|
|
50
|
-
private static let sizeColW: CGFloat = 80
|
|
51
|
-
private static let tileColW: CGFloat = 60
|
|
52
|
-
|
|
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())
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private func resolvedContentWidth(in availableWidth: CGFloat) -> CGFloat {
|
|
78
|
-
if isDesktopInventory {
|
|
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
|
|
86
|
-
return min(ideal, screenWidth * 0.92)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if isEmbedded {
|
|
90
|
-
return min(720, max(availableWidth - 32, 580))
|
|
91
|
-
}
|
|
92
|
-
return 580
|
|
93
|
-
}
|
|
94
|
-
|
|
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 {
|
|
102
|
-
VStack(spacing: 0) {
|
|
103
|
-
header
|
|
104
|
-
divider
|
|
105
|
-
if isDesktopInventory && state.desktopMode == .gridPreview {
|
|
106
|
-
gridPreviewContent
|
|
107
|
-
} else if isDesktopInventory {
|
|
108
|
-
desktopInventoryContent(contentWidth: contentWidth)
|
|
109
|
-
} else {
|
|
110
|
-
inventoryGrid
|
|
111
|
-
}
|
|
112
|
-
divider
|
|
113
|
-
chordFooter
|
|
114
|
-
}
|
|
115
|
-
.frame(width: contentWidth)
|
|
116
|
-
.background(Palette.bg)
|
|
117
|
-
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
118
|
-
.overlay(
|
|
119
|
-
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
120
|
-
.strokeBorder(Palette.borderLit, lineWidth: 0.5)
|
|
121
|
-
)
|
|
122
|
-
.overlay(executingOverlay)
|
|
123
|
-
.overlay(flashOverlay)
|
|
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)
|
|
132
|
-
}
|
|
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
|
-
)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// MARK: - Header
|
|
200
|
-
|
|
201
|
-
private var header: some View {
|
|
202
|
-
HStack {
|
|
203
|
-
Text(isDesktopInventory ? (state.isOrganizeFlow ? "ORGANIZE WINDOWS" : "DESKTOP INVENTORY") : "COMMAND MODE")
|
|
204
|
-
.font(Typo.monoBold(11))
|
|
205
|
-
.foregroundColor(Palette.text)
|
|
206
|
-
|
|
207
|
-
if isDesktopInventory && state.isOrganizeFlow {
|
|
208
|
-
bannerBadge("Current Space", tone: .neutral)
|
|
209
|
-
if let appName = state.organizeSeedAppName, !appName.isEmpty {
|
|
210
|
-
bannerBadge(appName, tone: .accent)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if isDesktopInventory {
|
|
215
|
-
Button(action: { state.copyInventoryToClipboard() }) {
|
|
216
|
-
HStack(spacing: 3) {
|
|
217
|
-
Image(systemName: "doc.on.doc")
|
|
218
|
-
.font(.system(size: 9))
|
|
219
|
-
Text("Copy")
|
|
220
|
-
.font(Typo.mono(9))
|
|
221
|
-
}
|
|
222
|
-
.foregroundColor(Palette.textDim)
|
|
223
|
-
.padding(.horizontal, 6)
|
|
224
|
-
.padding(.vertical, 3)
|
|
225
|
-
.background(
|
|
226
|
-
RoundedRectangle(cornerRadius: 3)
|
|
227
|
-
.fill(Palette.surface)
|
|
228
|
-
.overlay(
|
|
229
|
-
RoundedRectangle(cornerRadius: 3)
|
|
230
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
231
|
-
)
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
.buttonStyle(.plain)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
Spacer()
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
.padding(.horizontal, 16)
|
|
241
|
-
.padding(.vertical, 10)
|
|
242
|
-
.contentShape(Rectangle())
|
|
243
|
-
.gesture(
|
|
244
|
-
DragGesture()
|
|
245
|
-
.onChanged { _ in
|
|
246
|
-
CommandModeWindow.shared.panelWindow?.performDrag(with: NSApp.currentEvent!)
|
|
247
|
-
}
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// MARK: - Inventory Grid
|
|
252
|
-
|
|
253
|
-
private var inventoryGrid: some View {
|
|
254
|
-
ScrollView {
|
|
255
|
-
LazyVStack(alignment: .leading, spacing: 0) {
|
|
256
|
-
let items = state.inventory.items
|
|
257
|
-
if items.isEmpty {
|
|
258
|
-
emptyState
|
|
259
|
-
} else {
|
|
260
|
-
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
261
|
-
inventoryRow(item)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
.padding(.vertical, 6)
|
|
266
|
-
}
|
|
267
|
-
.frame(minHeight: 160, maxHeight: 240)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private var emptyState: some View {
|
|
271
|
-
HStack {
|
|
272
|
-
Spacer()
|
|
273
|
-
Text("No sessions found")
|
|
274
|
-
.font(Typo.mono(11))
|
|
275
|
-
.foregroundColor(Palette.textMuted)
|
|
276
|
-
Spacer()
|
|
277
|
-
}
|
|
278
|
-
.padding(.vertical, 24)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// MARK: - Desktop Inventory Content
|
|
282
|
-
|
|
283
|
-
private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
|
|
284
|
-
VStack(spacing: 0) {
|
|
285
|
-
if state.isOrganizeFlow {
|
|
286
|
-
organizeBanner
|
|
287
|
-
divider
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if state.isSearching {
|
|
291
|
-
searchBar
|
|
292
|
-
} else {
|
|
293
|
-
filterPillBar
|
|
294
|
-
}
|
|
295
|
-
divider
|
|
296
|
-
|
|
297
|
-
ZStack {
|
|
298
|
-
Group {
|
|
299
|
-
if let snapshot = state.filteredSnapshot, !snapshot.displays.isEmpty {
|
|
300
|
-
inventoryColumns(snapshot: snapshot, contentWidth: contentWidth)
|
|
301
|
-
} else {
|
|
302
|
-
desktopEmptyState
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
marqueeOverlay
|
|
307
|
-
}
|
|
308
|
-
.coordinateSpace(name: "inventoryPanel")
|
|
309
|
-
.background(
|
|
310
|
-
GeometryReader { geo in
|
|
311
|
-
Color.clear.onAppear {
|
|
312
|
-
panelOriginY = geo.frame(in: .global).origin.y
|
|
313
|
-
}
|
|
314
|
-
.onChange(of: geo.frame(in: .global).origin.y) { newY in
|
|
315
|
-
panelOriginY = newY
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
)
|
|
319
|
-
.onPreferenceChange(WindowRowFrameKey.self) { frames in
|
|
320
|
-
state.rowFrames = frames
|
|
321
|
-
}
|
|
322
|
-
.frame(maxHeight: .infinity)
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
@ViewBuilder
|
|
327
|
-
private func inventoryColumns(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> some View {
|
|
328
|
-
if shouldShowEmbeddedSidebar(snapshot: snapshot, contentWidth: contentWidth),
|
|
329
|
-
let display = snapshot.displays.first {
|
|
330
|
-
embeddedSingleDisplayLayout(display: display, contentWidth: contentWidth)
|
|
331
|
-
} else {
|
|
332
|
-
ScrollView(.horizontal, showsIndicators: false) {
|
|
333
|
-
HStack(alignment: .top, spacing: 0) {
|
|
334
|
-
let total = snapshot.displays.count
|
|
335
|
-
ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
|
|
336
|
-
if idx > 0 {
|
|
337
|
-
Rectangle()
|
|
338
|
-
.fill(Palette.border)
|
|
339
|
-
.frame(width: 0.5)
|
|
340
|
-
}
|
|
341
|
-
displayColumn(display, index: idx, total: total)
|
|
342
|
-
.frame(width: displayColumnWidth(for: contentWidth))
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private func shouldShowEmbeddedSidebar(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> Bool {
|
|
350
|
-
isEmbedded && snapshot.displays.count == 1 && contentWidth >= 900
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private func embeddedSingleDisplayLayout(
|
|
354
|
-
display: DesktopInventorySnapshot.DisplayInfo,
|
|
355
|
-
contentWidth: CGFloat
|
|
356
|
-
) -> some View {
|
|
357
|
-
let sidebarWidth = min(max(contentWidth * 0.28, 250), 330)
|
|
358
|
-
let mainWidth = max(contentWidth - sidebarWidth - 0.5, 620)
|
|
359
|
-
|
|
360
|
-
return HStack(alignment: .top, spacing: 0) {
|
|
361
|
-
displayColumn(display, index: 0, total: 1)
|
|
362
|
-
.frame(width: mainWidth)
|
|
363
|
-
|
|
364
|
-
Rectangle()
|
|
365
|
-
.fill(Palette.border)
|
|
366
|
-
.frame(width: 0.5)
|
|
367
|
-
|
|
368
|
-
embeddedInventorySidebar(display: display)
|
|
369
|
-
.frame(width: sidebarWidth)
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
private var organizeBanner: some View {
|
|
374
|
-
VStack(alignment: .leading, spacing: 6) {
|
|
375
|
-
HStack(alignment: .center, spacing: 8) {
|
|
376
|
-
Image(systemName: "rectangle.3.group")
|
|
377
|
-
.font(.system(size: 11, weight: .medium))
|
|
378
|
-
.foregroundColor(Palette.running)
|
|
379
|
-
Text(state.organizeSelectionSummary)
|
|
380
|
-
.font(Typo.monoBold(10))
|
|
381
|
-
.foregroundColor(Palette.text)
|
|
382
|
-
Spacer()
|
|
383
|
-
if state.selectedWindowIds.count > 1 {
|
|
384
|
-
bannerBadge("Ready", tone: .accent)
|
|
385
|
-
} else {
|
|
386
|
-
bannerBadge("Add More", tone: .neutral)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
Text(state.organizeGuidance)
|
|
391
|
-
.font(Typo.mono(10))
|
|
392
|
-
.foregroundColor(Palette.textDim)
|
|
393
|
-
.lineLimit(2)
|
|
394
|
-
}
|
|
395
|
-
.padding(.horizontal, 14)
|
|
396
|
-
.padding(.vertical, 8)
|
|
397
|
-
.background(Palette.running.opacity(0.06))
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
private var filterPillBar: some View {
|
|
401
|
-
HStack(spacing: 6) {
|
|
402
|
-
ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
|
|
403
|
-
let isActive = state.activePreset == preset
|
|
404
|
-
Button {
|
|
405
|
-
if isActive {
|
|
406
|
-
state.activePreset = nil
|
|
407
|
-
} else {
|
|
408
|
-
state.activePreset = preset
|
|
409
|
-
state.clearSelection()
|
|
410
|
-
}
|
|
411
|
-
} label: {
|
|
412
|
-
HStack(spacing: 3) {
|
|
413
|
-
Text(preset.rawValue)
|
|
414
|
-
.font(Typo.mono(9))
|
|
415
|
-
if let idx = preset.keyIndex {
|
|
416
|
-
Text("\(idx)")
|
|
417
|
-
.font(Typo.mono(8))
|
|
418
|
-
.foregroundColor(isActive ? Palette.text.opacity(0.7) : Palette.textMuted)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
.foregroundColor(isActive ? Palette.text : Palette.textDim)
|
|
422
|
-
.padding(.horizontal, 8)
|
|
423
|
-
.padding(.vertical, 4)
|
|
424
|
-
.background(
|
|
425
|
-
RoundedRectangle(cornerRadius: 10)
|
|
426
|
-
.fill(isActive ? Palette.running.opacity(0.2) : Palette.surface)
|
|
427
|
-
)
|
|
428
|
-
.overlay(
|
|
429
|
-
RoundedRectangle(cornerRadius: 10)
|
|
430
|
-
.strokeBorder(isActive ? Palette.running.opacity(0.4) : Palette.border, lineWidth: 0.5)
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
.buttonStyle(.plain)
|
|
434
|
-
}
|
|
435
|
-
Spacer()
|
|
436
|
-
}
|
|
437
|
-
.padding(.horizontal, 14)
|
|
438
|
-
.padding(.vertical, 6)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
private var searchBar: some View {
|
|
442
|
-
HStack(spacing: 10) {
|
|
443
|
-
Image(systemName: "magnifyingglass")
|
|
444
|
-
.font(.system(size: 11))
|
|
445
|
-
.foregroundColor(Palette.textDim)
|
|
446
|
-
TextField("Search windows & content…", text: $state.searchQuery)
|
|
447
|
-
.textFieldStyle(.plain)
|
|
448
|
-
.font(Typo.mono(12))
|
|
449
|
-
.foregroundColor(Palette.text)
|
|
450
|
-
.focused($isSearchFieldFocused)
|
|
451
|
-
if !state.searchQuery.isEmpty {
|
|
452
|
-
let total = state.flatWindowList.count
|
|
453
|
-
let ocrCount = state.ocrMatchSnippets.count
|
|
454
|
-
Text(ocrCount > 0 ? "\(total) matches (\(ocrCount) by content)" : "\(total) matches")
|
|
455
|
-
.font(Typo.mono(9))
|
|
456
|
-
.foregroundColor(Palette.textMuted)
|
|
457
|
-
}
|
|
458
|
-
Button(action: { state.deactivateSearch() }) {
|
|
459
|
-
Image(systemName: "xmark.circle.fill")
|
|
460
|
-
.font(.system(size: 11))
|
|
461
|
-
.foregroundColor(Palette.textDim)
|
|
462
|
-
}
|
|
463
|
-
.buttonStyle(.plain)
|
|
464
|
-
}
|
|
465
|
-
.padding(.horizontal, 14)
|
|
466
|
-
.padding(.vertical, 8)
|
|
467
|
-
.onAppear {
|
|
468
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
469
|
-
isSearchFieldFocused = true
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private func displayColumn(_ display: DesktopInventorySnapshot.DisplayInfo, index: Int, total: Int) -> some View {
|
|
475
|
-
VStack(alignment: .leading, spacing: 0) {
|
|
476
|
-
displayHeader(display, index: index, total: total)
|
|
477
|
-
divider
|
|
478
|
-
|
|
479
|
-
ScrollViewReader { proxy in
|
|
480
|
-
ScrollView {
|
|
481
|
-
LazyVStack(alignment: .leading, spacing: 0) {
|
|
482
|
-
ForEach(display.spaces) { space in
|
|
483
|
-
spaceHeader(space, display: display)
|
|
484
|
-
columnHeaders
|
|
485
|
-
ForEach(space.apps) { appGroup in
|
|
486
|
-
appGroupRows(appGroup, dimmed: !space.isCurrent)
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
.padding(.vertical, 4)
|
|
491
|
-
}
|
|
492
|
-
.onChange(of: state.selectedWindowIds) { newIds in
|
|
493
|
-
// Only scroll if the selected window is in this display
|
|
494
|
-
guard let id = newIds.first else { return }
|
|
495
|
-
let displayWindows = display.spaces.flatMap { $0.apps.flatMap { $0.windows } }
|
|
496
|
-
if displayWindows.contains(where: { $0.id == id }) {
|
|
497
|
-
withAnimation(.easeInOut(duration: 0.15)) {
|
|
498
|
-
proxy.scrollTo(id, anchor: .center)
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private var desktopEmptyState: some View {
|
|
507
|
-
HStack {
|
|
508
|
-
Spacer()
|
|
509
|
-
if state.isSearching && !state.searchQuery.isEmpty {
|
|
510
|
-
Text("No matches for \"\(state.searchQuery)\"")
|
|
511
|
-
.font(Typo.mono(11))
|
|
512
|
-
.foregroundColor(Palette.textMuted)
|
|
513
|
-
} else {
|
|
514
|
-
Text("No windows found")
|
|
515
|
-
.font(Typo.mono(11))
|
|
516
|
-
.foregroundColor(Palette.textMuted)
|
|
517
|
-
}
|
|
518
|
-
Spacer()
|
|
519
|
-
}
|
|
520
|
-
.padding(.vertical, 24)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
private func positionLabel(index: Int, total: Int) -> String {
|
|
524
|
-
if total == 2 { return index == 0 ? "Left" : "Right" }
|
|
525
|
-
if total == 3 { return ["Left", "Center", "Right"][index] }
|
|
526
|
-
return "\(index + 1) of \(total)"
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
private func displayHeader(_ display: DesktopInventorySnapshot.DisplayInfo, index: Int, total: Int) -> some View {
|
|
530
|
-
HStack(spacing: 6) {
|
|
531
|
-
Text(display.name)
|
|
532
|
-
.font(Typo.monoBold(11))
|
|
533
|
-
.foregroundColor(Palette.text)
|
|
534
|
-
if display.isMain {
|
|
535
|
-
Text("main")
|
|
536
|
-
.font(Typo.mono(8))
|
|
537
|
-
.foregroundColor(Palette.running.opacity(0.7))
|
|
538
|
-
.padding(.horizontal, 4)
|
|
539
|
-
.padding(.vertical, 1)
|
|
540
|
-
.background(
|
|
541
|
-
RoundedRectangle(cornerRadius: 2)
|
|
542
|
-
.fill(Palette.running.opacity(0.10))
|
|
543
|
-
)
|
|
544
|
-
}
|
|
545
|
-
if total > 1 {
|
|
546
|
-
Text(positionLabel(index: index, total: total))
|
|
547
|
-
.font(Typo.mono(9))
|
|
548
|
-
.foregroundColor(Palette.textDim)
|
|
549
|
-
}
|
|
550
|
-
Text("\(display.visibleFrame.w)×\(display.visibleFrame.h)")
|
|
551
|
-
.font(Typo.mono(9))
|
|
552
|
-
.foregroundColor(Palette.textDim)
|
|
553
|
-
Spacer()
|
|
554
|
-
Text("\(display.spaceCount) space\(display.spaceCount == 1 ? "" : "s")")
|
|
555
|
-
.font(Typo.mono(9))
|
|
556
|
-
.foregroundColor(Palette.textMuted)
|
|
557
|
-
}
|
|
558
|
-
.padding(.horizontal, 14)
|
|
559
|
-
.padding(.vertical, 8)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
private func embeddedInventorySidebar(display: DesktopInventorySnapshot.DisplayInfo) -> some View {
|
|
563
|
-
ScrollView {
|
|
564
|
-
VStack(alignment: .leading, spacing: 12) {
|
|
565
|
-
sidebarCard(title: "Overview") {
|
|
566
|
-
sidebarMetric(label: "Display", value: display.name)
|
|
567
|
-
sidebarMetric(
|
|
568
|
-
label: "Visible",
|
|
569
|
-
value: "\(display.visibleFrame.w)×\(display.visibleFrame.h)"
|
|
570
|
-
)
|
|
571
|
-
sidebarMetric(
|
|
572
|
-
label: "Current Space",
|
|
573
|
-
value: "Space \(display.currentSpaceIndex)"
|
|
574
|
-
)
|
|
575
|
-
sidebarMetric(
|
|
576
|
-
label: "Windows",
|
|
577
|
-
value: "\(windowCount(in: display)) total"
|
|
578
|
-
)
|
|
579
|
-
sidebarMetric(
|
|
580
|
-
label: "Apps",
|
|
581
|
-
value: "\(uniqueAppCount(in: display)) active"
|
|
582
|
-
)
|
|
583
|
-
sidebarMetric(
|
|
584
|
-
label: "Lattices",
|
|
585
|
-
value: "\(latticesWindowCount(in: display)) tagged"
|
|
586
|
-
)
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
sidebarCard(title: "Spaces") {
|
|
590
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
591
|
-
ForEach(display.spaces) { space in
|
|
592
|
-
HStack(alignment: .top, spacing: 8) {
|
|
593
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
594
|
-
HStack(spacing: 5) {
|
|
595
|
-
Text("Space \(space.index)")
|
|
596
|
-
.font(Typo.monoBold(10))
|
|
597
|
-
.foregroundColor(space.isCurrent ? Palette.running : Palette.text)
|
|
598
|
-
if space.isCurrent {
|
|
599
|
-
Text("active")
|
|
600
|
-
.font(Typo.mono(8))
|
|
601
|
-
.foregroundColor(Palette.running.opacity(0.75))
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
Text("\(spaceWindowCount(space)) windows across \(space.apps.count) apps")
|
|
605
|
-
.font(Typo.mono(9))
|
|
606
|
-
.foregroundColor(Palette.textDim)
|
|
607
|
-
}
|
|
608
|
-
Spacer()
|
|
609
|
-
Text("\(spaceLatticesCount(space))")
|
|
610
|
-
.font(Typo.mono(9))
|
|
611
|
-
.foregroundColor(Palette.textMuted)
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
sidebarCard(title: state.selectedWindowIds.isEmpty ? "Top Apps" : "Selection") {
|
|
618
|
-
if state.selectedWindowIds.isEmpty {
|
|
619
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
620
|
-
Text("Select a window to inspect it here.")
|
|
621
|
-
.font(Typo.mono(9))
|
|
622
|
-
.foregroundColor(Palette.textDim)
|
|
623
|
-
|
|
624
|
-
ForEach(topApps(in: display), id: \.name) { app in
|
|
625
|
-
HStack(spacing: 8) {
|
|
626
|
-
Text(app.name)
|
|
627
|
-
.font(Typo.monoBold(10))
|
|
628
|
-
.foregroundColor(Palette.text)
|
|
629
|
-
.lineLimit(1)
|
|
630
|
-
Spacer()
|
|
631
|
-
Text("\(app.count)")
|
|
632
|
-
.font(Typo.mono(9))
|
|
633
|
-
.foregroundColor(Palette.textMuted)
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
} else {
|
|
638
|
-
selectionSidebarContent
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
sidebarCard(title: "Keys") {
|
|
643
|
-
VStack(alignment: .leading, spacing: 7) {
|
|
644
|
-
sidebarShortcut("Arrows", "move through windows")
|
|
645
|
-
sidebarShortcut("/", "search by title or OCR")
|
|
646
|
-
sidebarShortcut("M", "jump to Screen Map")
|
|
647
|
-
sidebarShortcut("T", "tile selected window")
|
|
648
|
-
sidebarShortcut("Esc", "back or clear selection")
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
.padding(12)
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
@ViewBuilder
|
|
657
|
-
private var selectionSidebarContent: some View {
|
|
658
|
-
let selected = selectedWindows
|
|
659
|
-
|
|
660
|
-
if selected.count > 1 {
|
|
661
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
662
|
-
Text("\(selected.count) windows selected")
|
|
663
|
-
.font(Typo.monoBold(10))
|
|
664
|
-
.foregroundColor(Palette.text)
|
|
665
|
-
if !state.selectedWindowSummaryText.isEmpty {
|
|
666
|
-
Text(state.selectedWindowSummaryText)
|
|
667
|
-
.font(Typo.mono(9))
|
|
668
|
-
.foregroundColor(Palette.textDim)
|
|
669
|
-
}
|
|
670
|
-
ForEach(Array(selected.prefix(5)), id: \.id) { window in
|
|
671
|
-
HStack(spacing: 8) {
|
|
672
|
-
Text(window.appName ?? "Unknown")
|
|
673
|
-
.font(Typo.monoBold(9))
|
|
674
|
-
.foregroundColor(window.isLattices ? Palette.running : Palette.text)
|
|
675
|
-
.lineLimit(1)
|
|
676
|
-
Spacer()
|
|
677
|
-
Text(sizeText(window.frame))
|
|
678
|
-
.font(Typo.mono(9))
|
|
679
|
-
.foregroundColor(Palette.textMuted)
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
} else if let window = selected.first {
|
|
684
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
685
|
-
Text(window.appName ?? "Unknown")
|
|
686
|
-
.font(Typo.monoBold(10))
|
|
687
|
-
.foregroundColor(window.isLattices ? Palette.running : Palette.text)
|
|
688
|
-
Text(window.title.isEmpty ? "(untitled)" : window.title)
|
|
689
|
-
.font(Typo.mono(9))
|
|
690
|
-
.foregroundColor(Palette.textDim)
|
|
691
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
692
|
-
sidebarMetric(label: "Size", value: sizeText(window.frame))
|
|
693
|
-
if let tile = window.tilePosition?.label {
|
|
694
|
-
sidebarMetric(label: "Tile", value: tile)
|
|
695
|
-
}
|
|
696
|
-
if let session = window.latticesSession {
|
|
697
|
-
sidebarMetric(label: "Session", value: session)
|
|
698
|
-
}
|
|
699
|
-
if let path = window.inventoryPath {
|
|
700
|
-
Text(path.description)
|
|
701
|
-
.font(Typo.mono(8))
|
|
702
|
-
.foregroundColor(Palette.textMuted)
|
|
703
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
private func sidebarCard<Content: View>(
|
|
710
|
-
title: String,
|
|
711
|
-
@ViewBuilder content: () -> Content
|
|
712
|
-
) -> some View {
|
|
713
|
-
VStack(alignment: .leading, spacing: 10) {
|
|
714
|
-
Text(title.uppercased())
|
|
715
|
-
.font(Typo.mono(9))
|
|
716
|
-
.foregroundColor(Palette.textMuted)
|
|
717
|
-
content()
|
|
718
|
-
}
|
|
719
|
-
.padding(12)
|
|
720
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
721
|
-
.background(
|
|
722
|
-
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
723
|
-
.fill(Palette.surface.opacity(0.55))
|
|
724
|
-
.overlay(
|
|
725
|
-
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
726
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
727
|
-
)
|
|
728
|
-
)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
private func sidebarMetric(label: String, value: String) -> some View {
|
|
732
|
-
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
|
733
|
-
Text(label.uppercased())
|
|
734
|
-
.font(Typo.mono(8))
|
|
735
|
-
.foregroundColor(Palette.textMuted)
|
|
736
|
-
.frame(width: 74, alignment: .leading)
|
|
737
|
-
Text(value)
|
|
738
|
-
.font(Typo.mono(9))
|
|
739
|
-
.foregroundColor(Palette.text)
|
|
740
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
741
|
-
Spacer(minLength: 0)
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
private func sidebarShortcut(_ key: String, _ label: String) -> some View {
|
|
746
|
-
HStack(spacing: 8) {
|
|
747
|
-
Text(key)
|
|
748
|
-
.font(Typo.monoBold(9))
|
|
749
|
-
.foregroundColor(Palette.text)
|
|
750
|
-
.padding(.horizontal, 6)
|
|
751
|
-
.padding(.vertical, 3)
|
|
752
|
-
.background(
|
|
753
|
-
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
|
754
|
-
.fill(Palette.bg.opacity(0.8))
|
|
755
|
-
.overlay(
|
|
756
|
-
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
|
757
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
758
|
-
)
|
|
759
|
-
)
|
|
760
|
-
Text(label)
|
|
761
|
-
.font(Typo.mono(9))
|
|
762
|
-
.foregroundColor(Palette.textDim)
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
private func spaceHeader(_ space: DesktopInventorySnapshot.SpaceGroup, display: DesktopInventorySnapshot.DisplayInfo) -> some View {
|
|
767
|
-
HStack(spacing: 5) {
|
|
768
|
-
Text("Space \(space.index)")
|
|
769
|
-
.font(Typo.monoBold(10))
|
|
770
|
-
.foregroundColor(space.isCurrent ? Palette.running : Palette.textDim)
|
|
771
|
-
if space.isCurrent {
|
|
772
|
-
Text("active")
|
|
773
|
-
.font(Typo.mono(8))
|
|
774
|
-
.foregroundColor(Palette.running.opacity(0.7))
|
|
775
|
-
.padding(.horizontal, 4)
|
|
776
|
-
.padding(.vertical, 1)
|
|
777
|
-
.background(
|
|
778
|
-
RoundedRectangle(cornerRadius: 2)
|
|
779
|
-
.fill(Palette.running.opacity(0.10))
|
|
780
|
-
)
|
|
781
|
-
}
|
|
782
|
-
Spacer()
|
|
783
|
-
let windowCount = space.apps.reduce(0) { $0 + $1.windows.count }
|
|
784
|
-
Text("\(windowCount)")
|
|
785
|
-
.font(Typo.mono(9))
|
|
786
|
-
.foregroundColor(Palette.textMuted)
|
|
787
|
-
}
|
|
788
|
-
.padding(.horizontal, 14)
|
|
789
|
-
.padding(.top, 6)
|
|
790
|
-
.padding(.bottom, 2)
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
private var columnHeaders: some View {
|
|
794
|
-
HStack(spacing: 0) {
|
|
795
|
-
Text("APP / WINDOW")
|
|
796
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
797
|
-
Text("SIZE")
|
|
798
|
-
.frame(width: Self.sizeColW, alignment: .leading)
|
|
799
|
-
Text("TILE")
|
|
800
|
-
.frame(width: Self.tileColW, alignment: .trailing)
|
|
801
|
-
}
|
|
802
|
-
.font(Typo.mono(9))
|
|
803
|
-
.foregroundColor(Palette.textMuted)
|
|
804
|
-
.padding(.horizontal, 14)
|
|
805
|
-
.padding(.vertical, 3)
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
private func appGroupRows(_ appGroup: DesktopInventorySnapshot.AppGroup, dimmed: Bool = false) -> some View {
|
|
809
|
-
VStack(alignment: .leading, spacing: 0) {
|
|
810
|
-
if appGroup.windows.count == 1, let win = appGroup.windows.first {
|
|
811
|
-
inventoryRow(window: win, appLabel: appGroup.appName)
|
|
812
|
-
ocrSnippetRow(for: win.id)
|
|
813
|
-
if state.isSelected(win.id), let path = win.inventoryPath {
|
|
814
|
-
inventoryPathLabel(path)
|
|
815
|
-
}
|
|
816
|
-
} else {
|
|
817
|
-
Text(appGroup.appName)
|
|
818
|
-
.font(Typo.monoBold(10))
|
|
819
|
-
.foregroundColor(dimmed ? Palette.textDim : Palette.text)
|
|
820
|
-
.padding(.horizontal, 14)
|
|
821
|
-
.padding(.top, 4)
|
|
822
|
-
.padding(.bottom, 1)
|
|
823
|
-
ForEach(appGroup.windows) { win in
|
|
824
|
-
inventoryRow(window: win, indented: true)
|
|
825
|
-
ocrSnippetRow(for: win.id)
|
|
826
|
-
if state.isSelected(win.id), let path = win.inventoryPath {
|
|
827
|
-
inventoryPathLabel(path)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
.opacity(dimmed ? 0.6 : 1.0)
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
private func inventoryPathLabel(_ path: InventoryPath) -> some View {
|
|
836
|
-
Text(path.description)
|
|
837
|
-
.font(Typo.mono(8))
|
|
838
|
-
.foregroundColor(Palette.textMuted)
|
|
839
|
-
.padding(.horizontal, 28)
|
|
840
|
-
.padding(.vertical, 2)
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
@ViewBuilder
|
|
844
|
-
private func ocrSnippetRow(for windowId: UInt32) -> some View {
|
|
845
|
-
if let snippet = state.ocrMatchSnippets[windowId] {
|
|
846
|
-
HStack(spacing: 4) {
|
|
847
|
-
Image(systemName: "text.magnifyingglass")
|
|
848
|
-
.font(.system(size: 7))
|
|
849
|
-
.foregroundColor(Palette.textMuted)
|
|
850
|
-
Text(snippet)
|
|
851
|
-
.font(Typo.mono(9).italic())
|
|
852
|
-
.foregroundColor(Palette.textMuted)
|
|
853
|
-
.lineLimit(1)
|
|
854
|
-
.truncationMode(.tail)
|
|
855
|
-
}
|
|
856
|
-
.padding(.horizontal, 28)
|
|
857
|
-
.padding(.vertical, 1)
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/// Unified inventory row — handles both single-app rows (with appLabel) and
|
|
862
|
-
/// sub-rows under a multi-window app header (with indented).
|
|
863
|
-
private func inventoryRow(
|
|
864
|
-
window: DesktopInventorySnapshot.InventoryWindowInfo,
|
|
865
|
-
appLabel: String? = nil,
|
|
866
|
-
indented: Bool = false
|
|
867
|
-
) -> some View {
|
|
868
|
-
let isSelected = state.isSelected(window.id)
|
|
869
|
-
let isHovered = hoveredWindowId == window.id
|
|
870
|
-
let isLattices = window.isLattices
|
|
871
|
-
|
|
872
|
-
return HStack(spacing: 0) {
|
|
873
|
-
HStack(spacing: 4) {
|
|
874
|
-
if indented {
|
|
875
|
-
Spacer().frame(width: 8)
|
|
876
|
-
}
|
|
877
|
-
Group {
|
|
878
|
-
if isSelected {
|
|
879
|
-
Image(systemName: "checkmark.circle.fill")
|
|
880
|
-
.font(.system(size: 10, weight: .semibold))
|
|
881
|
-
.foregroundColor(Palette.running)
|
|
882
|
-
} else {
|
|
883
|
-
Text(isLattices ? "●" : "•")
|
|
884
|
-
.font(.system(size: 7))
|
|
885
|
-
.foregroundColor(isLattices ? Palette.running : Palette.textDim)
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
if let app = appLabel {
|
|
889
|
-
Text(app)
|
|
890
|
-
.font(Typo.monoBold(10))
|
|
891
|
-
.foregroundColor(isLattices ? Palette.running : Palette.text)
|
|
892
|
-
}
|
|
893
|
-
Text(windowTitle(window))
|
|
894
|
-
.font(Typo.mono(10))
|
|
895
|
-
.foregroundColor(
|
|
896
|
-
isLattices
|
|
897
|
-
? Palette.running.opacity(appLabel != nil && !isSelected ? 0.7 : 1.0)
|
|
898
|
-
: (isSelected ? Palette.text : Palette.textDim)
|
|
899
|
-
)
|
|
900
|
-
.lineLimit(1)
|
|
901
|
-
if isLattices, let session = window.latticesSession, appLabel == nil {
|
|
902
|
-
Text("[\(session)]")
|
|
903
|
-
.font(Typo.mono(9))
|
|
904
|
-
.foregroundColor(Palette.running.opacity(isSelected ? 1.0 : 0.6))
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
908
|
-
|
|
909
|
-
Text(sizeText(window.frame))
|
|
910
|
-
.font(Typo.mono(10))
|
|
911
|
-
.foregroundColor(isSelected ? Palette.text : Palette.textDim)
|
|
912
|
-
.frame(width: Self.sizeColW, alignment: .leading)
|
|
913
|
-
|
|
914
|
-
Text(window.tilePosition?.label ?? "\u{2014}")
|
|
915
|
-
.font(Typo.mono(10))
|
|
916
|
-
.foregroundColor(window.tilePosition != nil ? (isSelected ? Palette.text : Palette.textDim) : Palette.textMuted)
|
|
917
|
-
.frame(width: Self.tileColW, alignment: .trailing)
|
|
918
|
-
}
|
|
919
|
-
.padding(.horizontal, 14)
|
|
920
|
-
.padding(.vertical, 3)
|
|
921
|
-
.background(
|
|
922
|
-
RoundedRectangle(cornerRadius: 4)
|
|
923
|
-
.fill(isSelected ? Palette.surface : (isHovered ? Palette.surface.opacity(0.5) : Color.clear))
|
|
924
|
-
.padding(.horizontal, 6)
|
|
925
|
-
)
|
|
926
|
-
.overlay(
|
|
927
|
-
isSelected ?
|
|
928
|
-
RoundedRectangle(cornerRadius: 4)
|
|
929
|
-
.strokeBorder(Palette.borderLit, lineWidth: 0.5)
|
|
930
|
-
.padding(.horizontal, 6)
|
|
931
|
-
: nil
|
|
932
|
-
)
|
|
933
|
-
.background(
|
|
934
|
-
GeometryReader { geo in
|
|
935
|
-
Color.clear.preference(
|
|
936
|
-
key: WindowRowFrameKey.self,
|
|
937
|
-
value: [window.id: geo.frame(in: .named("inventoryPanel"))]
|
|
938
|
-
)
|
|
939
|
-
}
|
|
940
|
-
)
|
|
941
|
-
.contentShape(Rectangle())
|
|
942
|
-
.onTapGesture(count: 2) {
|
|
943
|
-
WindowTiler.navigateToWindowById(wid: window.id, pid: window.pid)
|
|
944
|
-
}
|
|
945
|
-
.onTapGesture(count: 1) {
|
|
946
|
-
let mods = NSEvent.modifierFlags
|
|
947
|
-
if mods.contains(.shift) {
|
|
948
|
-
state.selectRange(to: window.id)
|
|
949
|
-
} else if mods.contains(.command) {
|
|
950
|
-
state.toggleSelection(window.id)
|
|
951
|
-
} else {
|
|
952
|
-
state.selectSingle(window.id)
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
.contextMenu { windowContextMenu(for: window) }
|
|
956
|
-
.onHover { hovering in hoveredWindowId = hovering ? window.id : nil }
|
|
957
|
-
.id(window.id)
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// MARK: - Context Menu
|
|
961
|
-
|
|
962
|
-
@ViewBuilder
|
|
963
|
-
private func windowContextMenu(for window: DesktopInventorySnapshot.InventoryWindowInfo) -> some View {
|
|
964
|
-
let multiSelected = state.selectedWindowIds.count > 1 && state.isSelected(window.id)
|
|
965
|
-
let selCount = state.selectedWindowIds.count
|
|
966
|
-
|
|
967
|
-
if multiSelected {
|
|
968
|
-
// Multi-select context menu
|
|
969
|
-
Button {
|
|
970
|
-
state.showAndDistributeSelected()
|
|
971
|
-
} label: {
|
|
972
|
-
Label("Show & Distribute (\(selCount))", systemImage: "rectangle.3.group")
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
Button {
|
|
976
|
-
state.showAllSelected()
|
|
977
|
-
} label: {
|
|
978
|
-
Label("Show All (\(selCount))", systemImage: "macwindow.on.rectangle")
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
Button {
|
|
982
|
-
state.distributeSelected()
|
|
983
|
-
} label: {
|
|
984
|
-
Label("Distribute (\(selCount))", systemImage: "rectangle.split.3x1")
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
Divider()
|
|
988
|
-
|
|
989
|
-
Button {
|
|
990
|
-
state.focusAllSelected()
|
|
991
|
-
} label: {
|
|
992
|
-
Label("Focus All (\(selCount))", systemImage: "eye")
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
Button {
|
|
996
|
-
state.highlightAllSelected()
|
|
997
|
-
} label: {
|
|
998
|
-
Label("Highlight All (\(selCount))", systemImage: "sparkle")
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
Divider()
|
|
1002
|
-
|
|
1003
|
-
Menu("Tile All (\(selCount))") {
|
|
1004
|
-
ForEach(TilePosition.allCases) { tile in
|
|
1005
|
-
Button {
|
|
1006
|
-
state.showAndDistributeSelected(in: .tile(tile))
|
|
1007
|
-
} label: {
|
|
1008
|
-
Label(tile.label, systemImage: tile.icon)
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
Divider()
|
|
1014
|
-
|
|
1015
|
-
Button {
|
|
1016
|
-
state.clearSelection()
|
|
1017
|
-
} label: {
|
|
1018
|
-
Label("Deselect All", systemImage: "xmark.circle")
|
|
1019
|
-
}
|
|
1020
|
-
} else {
|
|
1021
|
-
// Single window context menu
|
|
1022
|
-
Button {
|
|
1023
|
-
WindowTiler.navigateToWindowById(wid: window.id, pid: window.pid)
|
|
1024
|
-
} label: {
|
|
1025
|
-
Label("Bring to Front", systemImage: "macwindow")
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
Button {
|
|
1029
|
-
WindowTiler.highlightWindowById(wid: window.id)
|
|
1030
|
-
} label: {
|
|
1031
|
-
Label("Highlight", systemImage: "sparkle")
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
Divider()
|
|
1035
|
-
|
|
1036
|
-
Menu("Tile Window") {
|
|
1037
|
-
ForEach(TilePosition.allCases) { tile in
|
|
1038
|
-
Button {
|
|
1039
|
-
WindowTiler.tileWindowById(wid: window.id, pid: window.pid, to: tile)
|
|
1040
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
1041
|
-
state.desktopSnapshot = nil
|
|
1042
|
-
}
|
|
1043
|
-
} label: {
|
|
1044
|
-
Label(tile.label, systemImage: tile.icon)
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
Divider()
|
|
1050
|
-
|
|
1051
|
-
Button {
|
|
1052
|
-
let info: String
|
|
1053
|
-
if let path = window.inventoryPath {
|
|
1054
|
-
info = path.description
|
|
1055
|
-
} else {
|
|
1056
|
-
let app = window.appName ?? "Unknown"
|
|
1057
|
-
let title = window.title.isEmpty ? "(untitled)" : window.title
|
|
1058
|
-
info = "[\(app)] \(title) wid=\(window.id)"
|
|
1059
|
-
}
|
|
1060
|
-
NSPasteboard.general.clearContents()
|
|
1061
|
-
NSPasteboard.general.setString(info, forType: .string)
|
|
1062
|
-
} label: {
|
|
1063
|
-
Label("Copy Info", systemImage: "doc.on.doc")
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
private func windowTitle(_ window: DesktopInventorySnapshot.InventoryWindowInfo) -> String {
|
|
1069
|
-
let title = window.title
|
|
1070
|
-
if title.isEmpty { return "(untitled)" }
|
|
1071
|
-
return title
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
private func sizeText(_ frame: WindowFrame) -> String {
|
|
1075
|
-
"\(Int(frame.w))×\(Int(frame.h))"
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
private var selectedWindows: [DesktopInventorySnapshot.InventoryWindowInfo] {
|
|
1079
|
-
state.flatWindowList.filter { state.selectedWindowIds.contains($0.id) }
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
private func windowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
|
|
1083
|
-
display.spaces.reduce(0) { total, space in
|
|
1084
|
-
total + spaceWindowCount(space)
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
private func uniqueAppCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
|
|
1089
|
-
Set(display.spaces.flatMap { $0.apps.map(\.appName) }).count
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
private func latticesWindowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
|
|
1093
|
-
display.spaces.reduce(0) { total, space in
|
|
1094
|
-
total + spaceLatticesCount(space)
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
private func spaceWindowCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
|
|
1099
|
-
space.apps.reduce(0) { total, app in
|
|
1100
|
-
total + app.windows.count
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
private func spaceLatticesCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
|
|
1105
|
-
space.apps.reduce(0) { total, app in
|
|
1106
|
-
total + app.windows.filter(\.isLattices).count
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
private func topApps(
|
|
1111
|
-
in display: DesktopInventorySnapshot.DisplayInfo
|
|
1112
|
-
) -> [(name: String, count: Int)] {
|
|
1113
|
-
var counts: [String: Int] = [:]
|
|
1114
|
-
for space in display.spaces {
|
|
1115
|
-
for app in space.apps {
|
|
1116
|
-
counts[app.appName, default: 0] += app.windows.count
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
return counts
|
|
1120
|
-
.map { (name: $0.key, count: $0.value) }
|
|
1121
|
-
.sorted { lhs, rhs in
|
|
1122
|
-
if lhs.count == rhs.count { return lhs.name < rhs.name }
|
|
1123
|
-
return lhs.count > rhs.count
|
|
1124
|
-
}
|
|
1125
|
-
.prefix(5)
|
|
1126
|
-
.map { $0 }
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/// Group items by their group label
|
|
1130
|
-
private var groupedItems: [(String, [CommandModeInventory.Item])] {
|
|
1131
|
-
var result: [(String, [CommandModeInventory.Item])] = []
|
|
1132
|
-
var seen = Set<String>()
|
|
1133
|
-
for item in state.inventory.items {
|
|
1134
|
-
if !seen.contains(item.group) {
|
|
1135
|
-
seen.insert(item.group)
|
|
1136
|
-
result.append((item.group, state.inventory.items.filter { $0.group == item.group }))
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
return result
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
private func sectionHeader(_ title: String) -> some View {
|
|
1143
|
-
Text(title.uppercased())
|
|
1144
|
-
.font(Typo.mono(9))
|
|
1145
|
-
.foregroundColor(Palette.textMuted)
|
|
1146
|
-
.padding(.horizontal, 16)
|
|
1147
|
-
.padding(.top, 10)
|
|
1148
|
-
.padding(.bottom, 4)
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
private func inventoryRow(_ item: CommandModeInventory.Item) -> some View {
|
|
1152
|
-
HStack(spacing: 0) {
|
|
1153
|
-
// Name
|
|
1154
|
-
Text(item.name)
|
|
1155
|
-
.font(Typo.mono(11))
|
|
1156
|
-
.foregroundColor(statusColor(item.status))
|
|
1157
|
-
.lineLimit(1)
|
|
1158
|
-
.frame(width: 160, alignment: .leading)
|
|
1159
|
-
|
|
1160
|
-
// Pane count
|
|
1161
|
-
Text(item.paneCount > 0 ? "\(item.paneCount) pane\(item.paneCount == 1 ? "" : "s")" : "—")
|
|
1162
|
-
.font(Typo.mono(10))
|
|
1163
|
-
.foregroundColor(Palette.textDim)
|
|
1164
|
-
.frame(width: 70, alignment: .leading)
|
|
1165
|
-
|
|
1166
|
-
// Status dot + label
|
|
1167
|
-
HStack(spacing: 4) {
|
|
1168
|
-
Circle()
|
|
1169
|
-
.fill(statusColor(item.status))
|
|
1170
|
-
.frame(width: 5, height: 5)
|
|
1171
|
-
Text(statusLabel(item.status))
|
|
1172
|
-
.font(Typo.mono(10))
|
|
1173
|
-
.foregroundColor(statusColor(item.status))
|
|
1174
|
-
}
|
|
1175
|
-
.frame(width: 80, alignment: .leading)
|
|
1176
|
-
|
|
1177
|
-
// Tile hint
|
|
1178
|
-
Text(item.tileHint ?? "\u{2014}")
|
|
1179
|
-
.font(Typo.mono(10))
|
|
1180
|
-
.foregroundColor(Palette.textMuted)
|
|
1181
|
-
.frame(width: 60, alignment: .leading)
|
|
1182
|
-
|
|
1183
|
-
Spacer()
|
|
1184
|
-
}
|
|
1185
|
-
.padding(.horizontal, 16)
|
|
1186
|
-
.padding(.vertical, 3)
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
private func statusColor(_ status: CommandModeInventory.Status) -> Color {
|
|
1190
|
-
switch status {
|
|
1191
|
-
case .running: return Palette.running
|
|
1192
|
-
case .attached: return Palette.running
|
|
1193
|
-
case .stopped: return Palette.textMuted
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
private func statusLabel(_ status: CommandModeInventory.Status) -> String {
|
|
1198
|
-
switch status {
|
|
1199
|
-
case .running: return "running"
|
|
1200
|
-
case .attached: return "attached"
|
|
1201
|
-
case .stopped: return "stopped"
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// MARK: - Chord Footer
|
|
1206
|
-
|
|
1207
|
-
private var chordFooter: some View {
|
|
1208
|
-
VStack(spacing: 4) {
|
|
1209
|
-
// Restore banner — shown when positions are saved
|
|
1210
|
-
if isDesktopInventory && state.savedPositions != nil {
|
|
1211
|
-
HStack(spacing: 10) {
|
|
1212
|
-
Text("Layout changed")
|
|
1213
|
-
.font(Typo.mono(10))
|
|
1214
|
-
.foregroundColor(Palette.text)
|
|
1215
|
-
Spacer()
|
|
1216
|
-
Button {
|
|
1217
|
-
state.restorePositions()
|
|
1218
|
-
} label: {
|
|
1219
|
-
HStack(spacing: 3) {
|
|
1220
|
-
Image(systemName: "arrow.uturn.backward")
|
|
1221
|
-
.font(.system(size: 9))
|
|
1222
|
-
Text("Restore")
|
|
1223
|
-
.font(Typo.mono(9))
|
|
1224
|
-
}
|
|
1225
|
-
.foregroundColor(Palette.text)
|
|
1226
|
-
.padding(.horizontal, 8)
|
|
1227
|
-
.padding(.vertical, 4)
|
|
1228
|
-
.background(
|
|
1229
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1230
|
-
.fill(Palette.surface)
|
|
1231
|
-
.overlay(
|
|
1232
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1233
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1234
|
-
)
|
|
1235
|
-
)
|
|
1236
|
-
}
|
|
1237
|
-
.buttonStyle(.plain)
|
|
1238
|
-
|
|
1239
|
-
Button {
|
|
1240
|
-
state.discardSavedPositions()
|
|
1241
|
-
} label: {
|
|
1242
|
-
HStack(spacing: 3) {
|
|
1243
|
-
Image(systemName: "checkmark")
|
|
1244
|
-
.font(.system(size: 9))
|
|
1245
|
-
Text("Keep")
|
|
1246
|
-
.font(Typo.mono(9))
|
|
1247
|
-
}
|
|
1248
|
-
.foregroundColor(Palette.running)
|
|
1249
|
-
.padding(.horizontal, 8)
|
|
1250
|
-
.padding(.vertical, 4)
|
|
1251
|
-
.background(
|
|
1252
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1253
|
-
.fill(Palette.running.opacity(0.1))
|
|
1254
|
-
.overlay(
|
|
1255
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1256
|
-
.strokeBorder(Palette.running.opacity(0.3), lineWidth: 0.5)
|
|
1257
|
-
)
|
|
1258
|
-
)
|
|
1259
|
-
}
|
|
1260
|
-
.buttonStyle(.plain)
|
|
1261
|
-
}
|
|
1262
|
-
.padding(.horizontal, 16)
|
|
1263
|
-
.padding(.vertical, 6)
|
|
1264
|
-
.background(Palette.running.opacity(0.05))
|
|
1265
|
-
divider
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
if isDesktopInventory && state.desktopMode == .gridPreview {
|
|
1269
|
-
// Grid preview hints
|
|
1270
|
-
HStack(spacing: 12) {
|
|
1271
|
-
chordHint(key: "←→↑↓", label: "region")
|
|
1272
|
-
chordHint(key: "1-7", label: "corners/thirds")
|
|
1273
|
-
chordHint(key: "c", label: "center")
|
|
1274
|
-
chordHint(key: "↩", label: "apply layout")
|
|
1275
|
-
chordHint(key: "s", label: "apply layout")
|
|
1276
|
-
chordHint(key: "esc", label: "cancel")
|
|
1277
|
-
Spacer()
|
|
1278
|
-
Text(state.gridPreviewRegionLabel.uppercased())
|
|
1279
|
-
.font(Typo.mono(9))
|
|
1280
|
-
.foregroundColor(Palette.textDim)
|
|
1281
|
-
let shape = state.gridPreviewShape
|
|
1282
|
-
Text(shape.map(String.init).joined(separator: " + "))
|
|
1283
|
-
.font(Typo.monoBold(9))
|
|
1284
|
-
.foregroundColor(Palette.running)
|
|
1285
|
-
}
|
|
1286
|
-
} else if isDesktopInventory && state.isSearching {
|
|
1287
|
-
// Search mode hints
|
|
1288
|
-
HStack(spacing: 12) {
|
|
1289
|
-
chordHint(key: "↩", label: "select & front")
|
|
1290
|
-
chordHint(key: "⌘A", label: "select all")
|
|
1291
|
-
chordHint(key: "⇧↑↓", label: "multi-select")
|
|
1292
|
-
if state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
|
|
1293
|
-
chordHint(key: "d", label: "organize")
|
|
1294
|
-
}
|
|
1295
|
-
if !state.selectedWindowIds.isEmpty {
|
|
1296
|
-
chordHint(key: "t", label: "tile")
|
|
1297
|
-
}
|
|
1298
|
-
chordHint(key: "esc", label: "exit search")
|
|
1299
|
-
Spacer()
|
|
1300
|
-
if state.selectedWindowIds.count > 1 {
|
|
1301
|
-
Text("\(state.selectedWindowIds.count) selected")
|
|
1302
|
-
.font(Typo.mono(9))
|
|
1303
|
-
.foregroundColor(Palette.running)
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
} else if isDesktopInventory && state.desktopMode == .tiling {
|
|
1307
|
-
// Tiling sub-mode hints
|
|
1308
|
-
HStack(spacing: 12) {
|
|
1309
|
-
if state.selectedWindowIds.count == 2 {
|
|
1310
|
-
chordHint(key: "←→", label: "split L/R")
|
|
1311
|
-
} else {
|
|
1312
|
-
chordHint(key: "←", label: "left")
|
|
1313
|
-
chordHint(key: "→", label: "right")
|
|
1314
|
-
}
|
|
1315
|
-
chordHint(key: "↑", label: "top")
|
|
1316
|
-
chordHint(key: "↓", label: "bottom")
|
|
1317
|
-
chordHint(key: "⇧↑", label: "max")
|
|
1318
|
-
chordHint(key: "1-4", label: "quad")
|
|
1319
|
-
chordHint(key: "5-7", label: "thirds")
|
|
1320
|
-
chordHint(key: "c", label: "center")
|
|
1321
|
-
if state.selectedWindowIds.count >= 2 {
|
|
1322
|
-
chordHint(key: "d", label: "distribute")
|
|
1323
|
-
}
|
|
1324
|
-
chordHint(key: "esc", label: "back")
|
|
1325
|
-
Spacer()
|
|
1326
|
-
if state.selectedWindowIds.count > 1 {
|
|
1327
|
-
Text("\(state.selectedWindowIds.count) windows")
|
|
1328
|
-
.font(Typo.mono(9))
|
|
1329
|
-
.foregroundColor(Palette.running)
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
} else if isDesktopInventory && state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
|
|
1333
|
-
HStack(spacing: 12) {
|
|
1334
|
-
chordHint(key: "d", label: "organize")
|
|
1335
|
-
chordHint(key: "⌘-click", label: "add/remove")
|
|
1336
|
-
chordHint(key: "⇧-click", label: "range")
|
|
1337
|
-
chordHint(key: "↩", label: "front")
|
|
1338
|
-
chordHint(key: "esc", label: "cancel")
|
|
1339
|
-
Spacer()
|
|
1340
|
-
Text("\(state.selectedWindowIds.count) selected")
|
|
1341
|
-
.font(Typo.mono(9))
|
|
1342
|
-
.foregroundColor(Palette.running)
|
|
1343
|
-
}
|
|
1344
|
-
} else if isDesktopInventory && state.isOrganizeFlow && !state.selectedWindowIds.isEmpty {
|
|
1345
|
-
HStack(spacing: 12) {
|
|
1346
|
-
chordHint(key: "⌘-click", label: "add more")
|
|
1347
|
-
chordHint(key: "d", label: "need 2+")
|
|
1348
|
-
chordHint(key: "↩", label: "front")
|
|
1349
|
-
chordHint(key: "esc", label: "cancel")
|
|
1350
|
-
Spacer()
|
|
1351
|
-
Text(state.organizeSelectionSummary)
|
|
1352
|
-
.font(Typo.mono(9))
|
|
1353
|
-
.foregroundColor(Palette.textDim)
|
|
1354
|
-
}
|
|
1355
|
-
} else if isDesktopInventory && state.isOrganizeFlow {
|
|
1356
|
-
HStack(spacing: 12) {
|
|
1357
|
-
chordHint(key: "click", label: "select")
|
|
1358
|
-
chordHint(key: "⌘-click", label: "add/remove")
|
|
1359
|
-
chordHint(key: "/", label: "search")
|
|
1360
|
-
chordHint(key: "esc", label: "cancel")
|
|
1361
|
-
Spacer()
|
|
1362
|
-
}
|
|
1363
|
-
} else if isDesktopInventory && state.selectedWindowIds.count > 1 {
|
|
1364
|
-
// Multi-selection active
|
|
1365
|
-
HStack(spacing: 12) {
|
|
1366
|
-
chordHint(key: "s", label: "grid preview")
|
|
1367
|
-
chordHint(key: "d", label: "distribute")
|
|
1368
|
-
chordHint(key: "s", label: "grid preview")
|
|
1369
|
-
chordHint(key: "↩", label: "front")
|
|
1370
|
-
chordHint(key: "t", label: "grid region")
|
|
1371
|
-
chordHint(key: "f", label: "focus")
|
|
1372
|
-
chordHint(key: "h", label: "highlight")
|
|
1373
|
-
chordHint(key: "esc", label: "clear")
|
|
1374
|
-
Spacer()
|
|
1375
|
-
if !state.selectedWindowSummaryText.isEmpty {
|
|
1376
|
-
Text(state.selectedWindowSummaryText)
|
|
1377
|
-
.font(Typo.mono(9))
|
|
1378
|
-
.foregroundColor(Palette.textDim)
|
|
1379
|
-
.lineLimit(1)
|
|
1380
|
-
}
|
|
1381
|
-
Text("\(state.selectedWindowIds.count) selected")
|
|
1382
|
-
.font(Typo.mono(9))
|
|
1383
|
-
.foregroundColor(Palette.running)
|
|
1384
|
-
}
|
|
1385
|
-
} else if isDesktopInventory && !state.selectedWindowIds.isEmpty {
|
|
1386
|
-
// Single selection active — browsing hints with direct shortcuts
|
|
1387
|
-
HStack(spacing: 12) {
|
|
1388
|
-
chordHint(key: "d", label: "organize")
|
|
1389
|
-
chordHint(key: "s", label: "show")
|
|
1390
|
-
chordHint(key: "↩", label: "front")
|
|
1391
|
-
chordHint(key: "f", label: "focus+close")
|
|
1392
|
-
chordHint(key: "t", label: "tile")
|
|
1393
|
-
chordHint(key: "h", label: "highlight")
|
|
1394
|
-
chordHint(key: "esc", label: "deselect")
|
|
1395
|
-
Spacer()
|
|
1396
|
-
}
|
|
1397
|
-
} else if isDesktopInventory {
|
|
1398
|
-
// No selection — browsing hints
|
|
1399
|
-
HStack(spacing: 12) {
|
|
1400
|
-
chordHint(key: "↑↓", label: "navigate")
|
|
1401
|
-
chordHint(key: "←→", label: "display")
|
|
1402
|
-
chordHint(key: "m", label: "map")
|
|
1403
|
-
chordHint(key: "/", label: "search")
|
|
1404
|
-
chordHint(key: "`", label: "chords")
|
|
1405
|
-
chordHint(key: "esc", label: "back")
|
|
1406
|
-
Spacer()
|
|
1407
|
-
}
|
|
1408
|
-
} else {
|
|
1409
|
-
// First row: action chords
|
|
1410
|
-
HStack(spacing: 12) {
|
|
1411
|
-
chordHint(key: "`", label: "desktop")
|
|
1412
|
-
ForEach(state.chords.prefix(3), id: \.key) { chord in
|
|
1413
|
-
chordHint(key: chord.key, label: chord.label)
|
|
1414
|
-
}
|
|
1415
|
-
Spacer()
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// Second row: layer chords + utility
|
|
1419
|
-
HStack(spacing: 12) {
|
|
1420
|
-
ForEach(state.chords.dropFirst(3), id: \.key) { chord in
|
|
1421
|
-
chordHint(key: chord.key, label: chord.label)
|
|
1422
|
-
}
|
|
1423
|
-
chordHint(key: "esc", label: "dismiss")
|
|
1424
|
-
Spacer()
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
.padding(.horizontal, 16)
|
|
1429
|
-
.padding(.vertical, 8)
|
|
1430
|
-
.background(Palette.surface.opacity(0.4))
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
private func chordHint(key: String, label: String) -> some View {
|
|
1434
|
-
HStack(spacing: 4) {
|
|
1435
|
-
Text(key)
|
|
1436
|
-
.font(Typo.mono(9))
|
|
1437
|
-
.foregroundColor(Palette.text)
|
|
1438
|
-
.padding(.horizontal, 4)
|
|
1439
|
-
.padding(.vertical, 2)
|
|
1440
|
-
.background(
|
|
1441
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1442
|
-
.fill(Palette.surface)
|
|
1443
|
-
.overlay(
|
|
1444
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1445
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1446
|
-
)
|
|
1447
|
-
)
|
|
1448
|
-
Text(label)
|
|
1449
|
-
.font(Typo.mono(9))
|
|
1450
|
-
.foregroundColor(Palette.textMuted)
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
private enum BannerTone {
|
|
1455
|
-
case neutral
|
|
1456
|
-
case accent
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
private func bannerBadge(_ text: String, tone: BannerTone) -> some View {
|
|
1460
|
-
let foreground = tone == .accent ? Palette.running : Palette.textDim
|
|
1461
|
-
let fill = tone == .accent ? Palette.running.opacity(0.10) : Palette.surface
|
|
1462
|
-
let stroke = tone == .accent ? Palette.running.opacity(0.30) : Palette.border
|
|
1463
|
-
|
|
1464
|
-
return Text(text)
|
|
1465
|
-
.font(Typo.mono(8))
|
|
1466
|
-
.foregroundColor(foreground)
|
|
1467
|
-
.padding(.horizontal, 6)
|
|
1468
|
-
.padding(.vertical, 3)
|
|
1469
|
-
.background(
|
|
1470
|
-
RoundedRectangle(cornerRadius: 8)
|
|
1471
|
-
.fill(fill)
|
|
1472
|
-
.overlay(
|
|
1473
|
-
RoundedRectangle(cornerRadius: 8)
|
|
1474
|
-
.strokeBorder(stroke, lineWidth: 0.5)
|
|
1475
|
-
)
|
|
1476
|
-
)
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
private func actionButton(key: String, label: String, action: @escaping () -> Void) -> some View {
|
|
1480
|
-
Button(action: action) {
|
|
1481
|
-
HStack(spacing: 4) {
|
|
1482
|
-
Text(key)
|
|
1483
|
-
.font(Typo.mono(9))
|
|
1484
|
-
.foregroundColor(Palette.text)
|
|
1485
|
-
.padding(.horizontal, 4)
|
|
1486
|
-
.padding(.vertical, 2)
|
|
1487
|
-
.background(
|
|
1488
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1489
|
-
.fill(Palette.surface)
|
|
1490
|
-
.overlay(
|
|
1491
|
-
RoundedRectangle(cornerRadius: 3)
|
|
1492
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1493
|
-
)
|
|
1494
|
-
)
|
|
1495
|
-
Text(label)
|
|
1496
|
-
.font(Typo.mono(9))
|
|
1497
|
-
.foregroundColor(Palette.textMuted)
|
|
1498
|
-
}
|
|
1499
|
-
.padding(.horizontal, 4)
|
|
1500
|
-
.padding(.vertical, 2)
|
|
1501
|
-
.background(
|
|
1502
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1503
|
-
.fill(Color.white.opacity(0.001))
|
|
1504
|
-
)
|
|
1505
|
-
.contentShape(Rectangle())
|
|
1506
|
-
}
|
|
1507
|
-
.buttonStyle(.plain)
|
|
1508
|
-
.onHover { hovering in
|
|
1509
|
-
if hovering {
|
|
1510
|
-
NSCursor.pointingHand.push()
|
|
1511
|
-
} else {
|
|
1512
|
-
NSCursor.pop()
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// MARK: - Executing Overlay
|
|
1518
|
-
|
|
1519
|
-
@ViewBuilder
|
|
1520
|
-
private var executingOverlay: some View {
|
|
1521
|
-
if case .executing(let label) = state.phase {
|
|
1522
|
-
ZStack {
|
|
1523
|
-
Palette.bg.opacity(0.85)
|
|
1524
|
-
HStack(spacing: 8) {
|
|
1525
|
-
Image(systemName: "checkmark.circle.fill")
|
|
1526
|
-
.font(.system(size: 16))
|
|
1527
|
-
.foregroundColor(Palette.running)
|
|
1528
|
-
Text(label)
|
|
1529
|
-
.font(Typo.monoBold(13))
|
|
1530
|
-
.foregroundColor(Palette.running)
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
1534
|
-
.transition(.opacity)
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// MARK: - Flash Overlay
|
|
1539
|
-
|
|
1540
|
-
@ViewBuilder
|
|
1541
|
-
private var flashOverlay: some View {
|
|
1542
|
-
if let msg = state.flashMessage {
|
|
1543
|
-
VStack {
|
|
1544
|
-
Spacer()
|
|
1545
|
-
HStack(spacing: 6) {
|
|
1546
|
-
Image(systemName: "rectangle.3.group")
|
|
1547
|
-
.font(.system(size: 11))
|
|
1548
|
-
Text(msg)
|
|
1549
|
-
.font(Typo.monoBold(11))
|
|
1550
|
-
}
|
|
1551
|
-
.foregroundColor(Palette.text)
|
|
1552
|
-
.padding(.horizontal, 14)
|
|
1553
|
-
.padding(.vertical, 8)
|
|
1554
|
-
.background(
|
|
1555
|
-
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
1556
|
-
.fill(Palette.surface)
|
|
1557
|
-
.overlay(
|
|
1558
|
-
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
1559
|
-
.strokeBorder(Palette.running.opacity(0.3), lineWidth: 0.5)
|
|
1560
|
-
)
|
|
1561
|
-
.shadow(color: .black.opacity(0.2), radius: 8, y: 2)
|
|
1562
|
-
)
|
|
1563
|
-
.padding(.bottom, 60)
|
|
1564
|
-
}
|
|
1565
|
-
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
|
1566
|
-
.animation(.easeOut(duration: 0.2), value: state.flashMessage)
|
|
1567
|
-
.allowsHitTesting(false)
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// MARK: - Divider
|
|
1572
|
-
|
|
1573
|
-
private var divider: some View {
|
|
1574
|
-
Rectangle()
|
|
1575
|
-
.fill(Palette.border)
|
|
1576
|
-
.frame(height: 0.5)
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// MARK: - Grid Preview
|
|
1580
|
-
|
|
1581
|
-
private var gridPreviewContent: some View {
|
|
1582
|
-
let windows = state.gridPreviewWindows
|
|
1583
|
-
let shape = state.gridPreviewShape
|
|
1584
|
-
let gridDesc = shape.map(String.init).joined(separator: " + ")
|
|
1585
|
-
|
|
1586
|
-
return VStack(spacing: 0) {
|
|
1587
|
-
// Title bar
|
|
1588
|
-
HStack {
|
|
1589
|
-
Text("LAYOUT PREVIEW")
|
|
1590
|
-
.font(Typo.monoBold(10))
|
|
1591
|
-
.foregroundColor(Palette.textDim)
|
|
1592
|
-
Text(state.gridPreviewRegionLabel.uppercased())
|
|
1593
|
-
.font(Typo.mono(9))
|
|
1594
|
-
.foregroundColor(Palette.textMuted)
|
|
1595
|
-
Text(gridDesc)
|
|
1596
|
-
.font(Typo.monoBold(10))
|
|
1597
|
-
.foregroundColor(Palette.running)
|
|
1598
|
-
Spacer()
|
|
1599
|
-
Text("\(windows.count) window\(windows.count == 1 ? "" : "s")")
|
|
1600
|
-
.font(Typo.mono(9))
|
|
1601
|
-
.foregroundColor(Palette.textMuted)
|
|
1602
|
-
}
|
|
1603
|
-
.padding(.horizontal, 16)
|
|
1604
|
-
.padding(.vertical, 8)
|
|
1605
|
-
|
|
1606
|
-
divider
|
|
1607
|
-
|
|
1608
|
-
// Screen map: current positions (dimmed) + target grid (bright)
|
|
1609
|
-
screenMap(windows: windows, shape: shape, placement: state.gridPreviewPlacement)
|
|
1610
|
-
.frame(height: 160)
|
|
1611
|
-
.padding(.horizontal, 12)
|
|
1612
|
-
.padding(.vertical, 8)
|
|
1613
|
-
|
|
1614
|
-
divider
|
|
1615
|
-
|
|
1616
|
-
// Grid cells with window details
|
|
1617
|
-
VStack(spacing: 2) {
|
|
1618
|
-
ForEach(Array(shape.enumerated()), id: \.offset) { rowIdx, colCount in
|
|
1619
|
-
HStack(spacing: 2) {
|
|
1620
|
-
ForEach(0..<colCount, id: \.self) { colIdx in
|
|
1621
|
-
let idx = shape[0..<rowIdx].reduce(0, +) + colIdx
|
|
1622
|
-
if idx < windows.count {
|
|
1623
|
-
gridCell(windows[idx], index: idx + 1)
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
.padding(8)
|
|
1630
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
// MARK: - Grid Preview Screen Map
|
|
1636
|
-
|
|
1637
|
-
/// Miniature proportional map of the screen showing current window positions and target grid slots
|
|
1638
|
-
private func screenMap(
|
|
1639
|
-
windows: [DesktopInventorySnapshot.InventoryWindowInfo],
|
|
1640
|
-
shape: [Int],
|
|
1641
|
-
placement: PlacementSpec?
|
|
1642
|
-
) -> some View {
|
|
1643
|
-
GeometryReader { geo in
|
|
1644
|
-
let availW = geo.size.width
|
|
1645
|
-
let availH = geo.size.height
|
|
1646
|
-
|
|
1647
|
-
// Get screen dimensions from snapshot
|
|
1648
|
-
let display = state.filteredSnapshot?.displays.first
|
|
1649
|
-
let screenW = CGFloat(display?.visibleFrame.w ?? 3440)
|
|
1650
|
-
let screenH = CGFloat(display?.visibleFrame.h ?? 1440)
|
|
1651
|
-
|
|
1652
|
-
// Scale to fit
|
|
1653
|
-
let scaleX = availW / screenW
|
|
1654
|
-
let scaleY = availH / screenH
|
|
1655
|
-
let scale = min(scaleX, scaleY)
|
|
1656
|
-
let mapW = screenW * scale
|
|
1657
|
-
let mapH = screenH * scale
|
|
1658
|
-
let offsetX = (availW - mapW) / 2
|
|
1659
|
-
let offsetY = (availH - mapH) / 2
|
|
1660
|
-
|
|
1661
|
-
ZStack(alignment: .topLeading) {
|
|
1662
|
-
// Screen background
|
|
1663
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1664
|
-
.fill(Palette.bg.opacity(0.5))
|
|
1665
|
-
.overlay(
|
|
1666
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1667
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
1668
|
-
)
|
|
1669
|
-
.frame(width: mapW, height: mapH)
|
|
1670
|
-
|
|
1671
|
-
if let placement {
|
|
1672
|
-
let region = placement.fractions
|
|
1673
|
-
RoundedRectangle(cornerRadius: 4)
|
|
1674
|
-
.strokeBorder(Palette.running.opacity(0.35), style: StrokeStyle(lineWidth: 1, dash: [6, 4]))
|
|
1675
|
-
.frame(width: mapW * region.2, height: mapH * region.3)
|
|
1676
|
-
.offset(x: mapW * region.0, y: mapH * region.1)
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// Current positions (dimmed)
|
|
1680
|
-
ForEach(Array(windows.enumerated()), id: \.element.id) { idx, win in
|
|
1681
|
-
let f = win.frame
|
|
1682
|
-
let x = CGFloat(f.x) * scale
|
|
1683
|
-
let y = CGFloat(f.y) * scale
|
|
1684
|
-
let w = max(CGFloat(f.w) * scale, 2)
|
|
1685
|
-
let h = max(CGFloat(f.h) * scale, 2)
|
|
1686
|
-
|
|
1687
|
-
RoundedRectangle(cornerRadius: 2)
|
|
1688
|
-
.fill(Palette.textMuted.opacity(0.15))
|
|
1689
|
-
.overlay(
|
|
1690
|
-
RoundedRectangle(cornerRadius: 2)
|
|
1691
|
-
.strokeBorder(Palette.textMuted.opacity(0.3), lineWidth: 0.5)
|
|
1692
|
-
)
|
|
1693
|
-
.frame(width: w, height: h)
|
|
1694
|
-
.offset(x: x, y: y)
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// Target grid slots (bright)
|
|
1698
|
-
let slots = computeMapSlots(
|
|
1699
|
-
count: windows.count,
|
|
1700
|
-
shape: shape,
|
|
1701
|
-
mapW: mapW,
|
|
1702
|
-
mapH: mapH,
|
|
1703
|
-
region: placement?.fractions
|
|
1704
|
-
)
|
|
1705
|
-
ForEach(Array(slots.enumerated()), id: \.offset) { idx, slot in
|
|
1706
|
-
let win = idx < windows.count ? windows[idx] : nil
|
|
1707
|
-
RoundedRectangle(cornerRadius: 2)
|
|
1708
|
-
.fill(Palette.running.opacity(0.12))
|
|
1709
|
-
.overlay(
|
|
1710
|
-
RoundedRectangle(cornerRadius: 2)
|
|
1711
|
-
.strokeBorder(Palette.running.opacity(0.5), lineWidth: 1)
|
|
1712
|
-
)
|
|
1713
|
-
.overlay {
|
|
1714
|
-
VStack(spacing: 1) {
|
|
1715
|
-
Text("\(idx + 1)")
|
|
1716
|
-
.font(Typo.monoBold(9))
|
|
1717
|
-
.foregroundColor(Palette.running)
|
|
1718
|
-
if let win = win {
|
|
1719
|
-
Text(win.appName ?? "")
|
|
1720
|
-
.font(Typo.mono(7))
|
|
1721
|
-
.foregroundColor(Palette.running.opacity(0.7))
|
|
1722
|
-
.lineLimit(1)
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
.frame(width: slot.width - 2, height: slot.height - 2)
|
|
1727
|
-
.offset(x: slot.origin.x + 1, y: slot.origin.y + 1)
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
.offset(x: offsetX, y: offsetY)
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
/// Compute grid slots scaled to the mini map dimensions
|
|
1735
|
-
private func computeMapSlots(
|
|
1736
|
-
count: Int,
|
|
1737
|
-
shape: [Int],
|
|
1738
|
-
mapW: CGFloat,
|
|
1739
|
-
mapH: CGFloat,
|
|
1740
|
-
region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil
|
|
1741
|
-
) -> [CGRect] {
|
|
1742
|
-
let regionX = mapW * (region?.0 ?? 0)
|
|
1743
|
-
let regionY = mapH * (region?.1 ?? 0)
|
|
1744
|
-
let regionW = mapW * (region?.2 ?? 1)
|
|
1745
|
-
let regionH = mapH * (region?.3 ?? 1)
|
|
1746
|
-
let rowCount = shape.count
|
|
1747
|
-
let rowH = regionH / CGFloat(rowCount)
|
|
1748
|
-
var slots: [CGRect] = []
|
|
1749
|
-
for (row, cols) in shape.enumerated() {
|
|
1750
|
-
let colW = regionW / CGFloat(cols)
|
|
1751
|
-
let y = regionY + CGFloat(row) * rowH
|
|
1752
|
-
for col in 0..<cols {
|
|
1753
|
-
slots.append(CGRect(
|
|
1754
|
-
x: regionX + CGFloat(col) * colW,
|
|
1755
|
-
y: y,
|
|
1756
|
-
width: colW,
|
|
1757
|
-
height: rowH
|
|
1758
|
-
))
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
return slots
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
private func gridCell(_ window: DesktopInventorySnapshot.InventoryWindowInfo, index: Int) -> some View {
|
|
1765
|
-
VStack(spacing: 3) {
|
|
1766
|
-
// App name
|
|
1767
|
-
Text(window.appName ?? "Unknown")
|
|
1768
|
-
.font(Typo.monoBold(10))
|
|
1769
|
-
.foregroundColor(window.isLattices ? Palette.running : Palette.text)
|
|
1770
|
-
.lineLimit(1)
|
|
1771
|
-
|
|
1772
|
-
// Window title
|
|
1773
|
-
Text(windowTitle(window))
|
|
1774
|
-
.font(Typo.mono(9))
|
|
1775
|
-
.foregroundColor(Palette.textDim)
|
|
1776
|
-
.lineLimit(2)
|
|
1777
|
-
.multilineTextAlignment(.center)
|
|
1778
|
-
|
|
1779
|
-
// Size
|
|
1780
|
-
Text(sizeText(window.frame))
|
|
1781
|
-
.font(Typo.mono(8))
|
|
1782
|
-
.foregroundColor(Palette.textMuted)
|
|
1783
|
-
}
|
|
1784
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
1785
|
-
.padding(.vertical, 8)
|
|
1786
|
-
.padding(.horizontal, 6)
|
|
1787
|
-
.background(
|
|
1788
|
-
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
1789
|
-
.fill(Palette.surface)
|
|
1790
|
-
)
|
|
1791
|
-
.overlay(
|
|
1792
|
-
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
1793
|
-
.strokeBorder(window.isLattices ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
|
|
1794
|
-
)
|
|
1795
|
-
.overlay(alignment: .topLeading) {
|
|
1796
|
-
Text("\(index)")
|
|
1797
|
-
.font(Typo.mono(8))
|
|
1798
|
-
.foregroundColor(Palette.textMuted)
|
|
1799
|
-
.padding(4)
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
// MARK: - Marquee Overlay
|
|
1804
|
-
|
|
1805
|
-
@ViewBuilder
|
|
1806
|
-
private var marqueeOverlay: some View {
|
|
1807
|
-
if state.isDragging {
|
|
1808
|
-
let rect = state.marqueeRect
|
|
1809
|
-
Rectangle()
|
|
1810
|
-
.fill(Palette.running.opacity(0.08))
|
|
1811
|
-
.overlay(
|
|
1812
|
-
Rectangle()
|
|
1813
|
-
.strokeBorder(Palette.running.opacity(0.4), lineWidth: 1)
|
|
1814
|
-
)
|
|
1815
|
-
.frame(width: rect.width, height: rect.height)
|
|
1816
|
-
.position(x: rect.midX, y: rect.midY)
|
|
1817
|
-
.allowsHitTesting(false)
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// MARK: - Key Handler
|
|
1822
|
-
|
|
1823
|
-
private func installKeyHandler() {
|
|
1824
|
-
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
|
|
1825
|
-
guard state.phase == .inventory || state.phase == .desktopInventory else { return event }
|
|
1826
|
-
// Only handle keys when our panel is the key window
|
|
1827
|
-
guard let panel = CommandModeWindow.shared.panelWindow,
|
|
1828
|
-
panel.isKeyWindow else { return event }
|
|
1829
|
-
let consumed = state.handleKey(event.keyCode, modifiers: event.modifierFlags)
|
|
1830
|
-
return consumed ? nil : event
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
// MARK: - Mouse Monitors (marquee drag + screen map drag)
|
|
1835
|
-
|
|
1836
|
-
private func installMouseMonitors() {
|
|
1837
|
-
let dragThreshold: CGFloat = 4
|
|
1838
|
-
|
|
1839
|
-
mouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { event in
|
|
1840
|
-
guard let eventWindow = event.window,
|
|
1841
|
-
eventWindow === CommandModeWindow.shared.panelWindow else { return event }
|
|
1842
|
-
guard state.phase == .desktopInventory else { return event }
|
|
1843
|
-
|
|
1844
|
-
state.dragStartPoint = event.locationInWindow
|
|
1845
|
-
return event
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
mouseDragMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDragged) { event in
|
|
1849
|
-
guard state.phase == .desktopInventory else { return event }
|
|
1850
|
-
|
|
1851
|
-
guard let startPt = state.dragStartPoint else { return event }
|
|
1852
|
-
|
|
1853
|
-
let currentPt = event.locationInWindow
|
|
1854
|
-
|
|
1855
|
-
if !state.isDragging {
|
|
1856
|
-
// Check threshold before starting drag
|
|
1857
|
-
let dx = currentPt.x - startPt.x
|
|
1858
|
-
let dy = currentPt.y - startPt.y
|
|
1859
|
-
let dist = sqrt(dx * dx + dy * dy)
|
|
1860
|
-
guard dist >= dragThreshold else { return event }
|
|
1861
|
-
|
|
1862
|
-
// Convert NSEvent bottom-left → SwiftUI top-left in inventoryPanel space
|
|
1863
|
-
let additive = event.modifierFlags.contains(.command)
|
|
1864
|
-
let swiftUIStart = convertToPanel(startPt, event: event)
|
|
1865
|
-
state.beginDrag(at: swiftUIStart, additive: additive)
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
let swiftUICurrent = convertToPanel(currentPt, event: event)
|
|
1869
|
-
state.updateDrag(to: swiftUICurrent)
|
|
1870
|
-
|
|
1871
|
-
return nil // consume to prevent ScrollView scrolling during drag
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
mouseUpMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseUp) { event in
|
|
1875
|
-
if state.isDragging {
|
|
1876
|
-
state.endDrag()
|
|
1877
|
-
}
|
|
1878
|
-
state.dragStartPoint = nil
|
|
1879
|
-
return event
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
/// Convert NSEvent window coordinates (bottom-left origin) to SwiftUI inventoryPanel coordinates (top-left origin)
|
|
1887
|
-
private func convertToPanel(_ windowPoint: NSPoint, event: NSEvent) -> CGPoint {
|
|
1888
|
-
guard let nsWindow = event.window else { return .zero }
|
|
1889
|
-
// Convert to screen coordinates
|
|
1890
|
-
let screenPoint = nsWindow.convertPoint(toScreen: windowPoint)
|
|
1891
|
-
// Convert to SwiftUI top-left: screen Y is bottom-up, SwiftUI Y is top-down
|
|
1892
|
-
let screenHeight = NSScreen.main?.frame.height ?? 0
|
|
1893
|
-
let flippedY = screenHeight - screenPoint.y
|
|
1894
|
-
// Subtract the panel's global origin to get panel-local coordinates
|
|
1895
|
-
let panelY = flippedY - panelOriginY
|
|
1896
|
-
// X is relative to window — we need global X minus panel X
|
|
1897
|
-
// For simplicity, use the window point X directly since the panel fills the window width
|
|
1898
|
-
return CGPoint(x: windowPoint.x, y: panelY)
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
/// Convert NSEvent to flipped window-local coordinates (Y=0 at top of window content)
|
|
1902
|
-
/// This matches SwiftUI GeometryReader's `.global` coordinate space inside NSHostingView
|
|
1903
|
-
private func flippedScreenPoint(_ event: NSEvent) -> CGPoint {
|
|
1904
|
-
guard let nsWindow = event.window else { return .zero }
|
|
1905
|
-
let loc = event.locationInWindow // bottom-left origin
|
|
1906
|
-
let windowHeight = nsWindow.contentView?.frame.height ?? nsWindow.frame.height
|
|
1907
|
-
return CGPoint(x: loc.x, y: windowHeight - loc.y)
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
private func removeMouseMonitors() {
|
|
1911
|
-
if let m = mouseDownMonitor { NSEvent.removeMonitor(m); mouseDownMonitor = nil }
|
|
1912
|
-
if let m = mouseDragMonitor { NSEvent.removeMonitor(m); mouseDragMonitor = nil }
|
|
1913
|
-
if let m = mouseUpMonitor { NSEvent.removeMonitor(m); mouseUpMonitor = nil }
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// Clear hover when leaving desktop inventory
|
|
1917
|
-
private func clearDesktopState() {
|
|
1918
|
-
hoveredWindowId = nil
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
private func removeKeyHandler() {
|
|
1922
|
-
if let monitor = eventMonitor {
|
|
1923
|
-
NSEvent.removeMonitor(monitor)
|
|
1924
|
-
eventMonitor = nil
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|