@lattices/cli 0.4.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/apps/mac/Info.plist +4 -4
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +5 -5
- package/docs/voice.md +11 -27
- package/package.json +11 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
|
-
|
|
3
|
-
struct TabGroupRow: View {
|
|
4
|
-
let group: TabGroup
|
|
5
|
-
@ObservedObject var workspace: WorkspaceManager
|
|
6
|
-
|
|
7
|
-
@State private var isHovered = false
|
|
8
|
-
@State private var isExpanded = false
|
|
9
|
-
|
|
10
|
-
private var isRunning: Bool { workspace.isGroupRunning(group) }
|
|
11
|
-
|
|
12
|
-
var body: some View {
|
|
13
|
-
VStack(spacing: 0) {
|
|
14
|
-
// Header row
|
|
15
|
-
HStack(spacing: 10) {
|
|
16
|
-
// Status bar
|
|
17
|
-
RoundedRectangle(cornerRadius: 1)
|
|
18
|
-
.fill(isRunning ? Palette.running : Palette.border)
|
|
19
|
-
.frame(width: 3, height: 32)
|
|
20
|
-
|
|
21
|
-
// Expand chevron
|
|
22
|
-
Button {
|
|
23
|
-
withAnimation(.easeOut(duration: 0.15)) { isExpanded.toggle() }
|
|
24
|
-
} label: {
|
|
25
|
-
Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
|
|
26
|
-
.font(.system(size: 9, weight: .semibold))
|
|
27
|
-
.foregroundColor(Palette.textMuted)
|
|
28
|
-
.frame(width: 14)
|
|
29
|
-
}
|
|
30
|
-
.buttonStyle(.plain)
|
|
31
|
-
|
|
32
|
-
// Info
|
|
33
|
-
VStack(alignment: .leading, spacing: 3) {
|
|
34
|
-
HStack(spacing: 6) {
|
|
35
|
-
Text(group.label)
|
|
36
|
-
.font(Typo.heading(13))
|
|
37
|
-
.foregroundColor(Palette.text)
|
|
38
|
-
.lineLimit(1)
|
|
39
|
-
|
|
40
|
-
Text("\(group.tabs.count) tabs")
|
|
41
|
-
.font(Typo.mono(9))
|
|
42
|
-
.foregroundColor(Palette.textMuted)
|
|
43
|
-
.padding(.horizontal, 5)
|
|
44
|
-
.padding(.vertical, 1)
|
|
45
|
-
.background(
|
|
46
|
-
RoundedRectangle(cornerRadius: 3)
|
|
47
|
-
.fill(Palette.surface)
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
Text(group.tabs.map { $0.label ?? ($0.path as NSString).lastPathComponent }.joined(separator: " \u{00B7} "))
|
|
52
|
-
.font(Typo.mono(10))
|
|
53
|
-
.foregroundColor(Palette.textMuted)
|
|
54
|
-
.lineLimit(1)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
Spacer()
|
|
58
|
-
|
|
59
|
-
// Actions
|
|
60
|
-
HStack(spacing: 4) {
|
|
61
|
-
if isRunning {
|
|
62
|
-
Button {
|
|
63
|
-
workspace.killGroup(group)
|
|
64
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
65
|
-
ProjectScanner.shared.refreshStatus()
|
|
66
|
-
}
|
|
67
|
-
} label: {
|
|
68
|
-
Text("Kill")
|
|
69
|
-
.angularButton(Palette.kill, filled: false)
|
|
70
|
-
}
|
|
71
|
-
.buttonStyle(.plain)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
Button {
|
|
75
|
-
if isRunning {
|
|
76
|
-
// Focus the first tab's session
|
|
77
|
-
if let firstTab = group.tabs.first {
|
|
78
|
-
let session = WorkspaceManager.sessionName(for: firstTab.path)
|
|
79
|
-
let terminal = Preferences.shared.terminal
|
|
80
|
-
terminal.focusOrAttach(session: session)
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
workspace.launchGroup(group)
|
|
84
|
-
}
|
|
85
|
-
} label: {
|
|
86
|
-
Text(isRunning ? "Attach" : "Launch")
|
|
87
|
-
.angularButton(isRunning ? Palette.running : Palette.launch)
|
|
88
|
-
}
|
|
89
|
-
.buttonStyle(.plain)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
.padding(.horizontal, 10)
|
|
93
|
-
.padding(.vertical, 8)
|
|
94
|
-
.glassCard(hovered: isHovered)
|
|
95
|
-
|
|
96
|
-
// Expanded tab list
|
|
97
|
-
if isExpanded {
|
|
98
|
-
VStack(spacing: 2) {
|
|
99
|
-
ForEach(Array(group.tabs.enumerated()), id: \.offset) { idx, tab in
|
|
100
|
-
tabRow(tab: tab, index: idx)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
.padding(.leading, 36)
|
|
104
|
-
.padding(.trailing, 10)
|
|
105
|
-
.padding(.vertical, 4)
|
|
106
|
-
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
.contentShape(Rectangle())
|
|
110
|
-
.onHover { isHovered = $0 }
|
|
111
|
-
.contextMenu {
|
|
112
|
-
if isRunning {
|
|
113
|
-
Button("Attach") {
|
|
114
|
-
if let firstTab = group.tabs.first {
|
|
115
|
-
let session = WorkspaceManager.sessionName(for: firstTab.path)
|
|
116
|
-
let terminal = Preferences.shared.terminal
|
|
117
|
-
terminal.focusOrAttach(session: session)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
Divider()
|
|
121
|
-
ForEach(Array(group.tabs.enumerated()), id: \.offset) { idx, tab in
|
|
122
|
-
Button("Go to: \(tab.label ?? (tab.path as NSString).lastPathComponent)") {
|
|
123
|
-
workspace.focusTab(group: group, tabIndex: idx)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
Divider()
|
|
127
|
-
Button("Kill Group") {
|
|
128
|
-
workspace.killGroup(group)
|
|
129
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
130
|
-
ProjectScanner.shared.refreshStatus()
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
Button("Launch") {
|
|
135
|
-
workspace.launchGroup(group)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private func tabRow(tab: TabGroupTab, index: Int) -> some View {
|
|
142
|
-
HStack(spacing: 8) {
|
|
143
|
-
Image(systemName: "rectangle.topthird.inset.filled")
|
|
144
|
-
.font(.system(size: 9))
|
|
145
|
-
.foregroundColor(isRunning ? Palette.running.opacity(0.7) : Palette.textMuted)
|
|
146
|
-
|
|
147
|
-
Text(tab.label ?? (tab.path as NSString).lastPathComponent)
|
|
148
|
-
.font(Typo.mono(11))
|
|
149
|
-
.foregroundColor(Palette.text)
|
|
150
|
-
.lineLimit(1)
|
|
151
|
-
|
|
152
|
-
Spacer()
|
|
153
|
-
|
|
154
|
-
if isRunning {
|
|
155
|
-
Button {
|
|
156
|
-
workspace.focusTab(group: group, tabIndex: index)
|
|
157
|
-
} label: {
|
|
158
|
-
Text("Go")
|
|
159
|
-
.font(Typo.mono(9))
|
|
160
|
-
.foregroundColor(Palette.textDim)
|
|
161
|
-
.padding(.horizontal, 6)
|
|
162
|
-
.padding(.vertical, 2)
|
|
163
|
-
.background(
|
|
164
|
-
RoundedRectangle(cornerRadius: 3)
|
|
165
|
-
.fill(Palette.surface)
|
|
166
|
-
.overlay(
|
|
167
|
-
RoundedRectangle(cornerRadius: 3)
|
|
168
|
-
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
169
|
-
)
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
.buttonStyle(.plain)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
.padding(.horizontal, 8)
|
|
176
|
-
.padding(.vertical, 4)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
|
-
|
|
3
|
-
// MARK: - Colors
|
|
4
|
-
|
|
5
|
-
enum Palette {
|
|
6
|
-
// Base surfaces
|
|
7
|
-
static let bg = Color(red: 0.08, green: 0.08, blue: 0.09) // #141416
|
|
8
|
-
static let bgSidebar = Color(red: 0.08, green: 0.08, blue: 0.09) // same as bg
|
|
9
|
-
static let surface = Color(white: 0.10) // Raised cards
|
|
10
|
-
static let surfaceHov = Color(white: 0.14) // Hovered cards
|
|
11
|
-
static let border = Color.white.opacity(0.08)
|
|
12
|
-
static let borderLit = Color.white.opacity(0.14)
|
|
13
|
-
|
|
14
|
-
// Text
|
|
15
|
-
static let text = Color.white.opacity(0.92)
|
|
16
|
-
static let textDim = Color.white.opacity(0.58)
|
|
17
|
-
static let textMuted = Color.white.opacity(0.40)
|
|
18
|
-
|
|
19
|
-
// Functional accents
|
|
20
|
-
static let running = Color(red: 0.20, green: 0.78, blue: 0.45) // Green
|
|
21
|
-
static let detach = Color(red: 0.96, green: 0.65, blue: 0.14) // Amber
|
|
22
|
-
static let kill = Color(red: 0.94, green: 0.30, blue: 0.35) // Red
|
|
23
|
-
static let launch = Color.white // Clean white
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// MARK: - Typography
|
|
27
|
-
|
|
28
|
-
enum Typo {
|
|
29
|
-
private static let jetbrains = "JetBrains Mono"
|
|
30
|
-
private static let geist = "GeistMono Nerd Font"
|
|
31
|
-
private static let gohu = "GohuFontuni14 Nerd Font"
|
|
32
|
-
|
|
33
|
-
static func title(_ size: CGFloat = 15) -> Font {
|
|
34
|
-
.system(size: size, weight: .bold, design: .rounded)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static func heading(_ size: CGFloat = 13) -> Font {
|
|
38
|
-
.system(size: size, weight: .semibold, design: .rounded)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
static func body(_ size: CGFloat = 12) -> Font {
|
|
42
|
-
.system(size: size, weight: .regular, design: .rounded)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
static func caption(_ size: CGFloat = 10) -> Font {
|
|
46
|
-
.system(size: size, weight: .medium, design: .rounded)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
static func mono(_ size: CGFloat = 11) -> Font {
|
|
50
|
-
.custom(jetbrains, size: size)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static func monoBold(_ size: CGFloat = 11) -> Font {
|
|
54
|
-
Font.custom(jetbrains, size: size).weight(.semibold)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
static func geistMono(_ size: CGFloat = 11) -> Font {
|
|
58
|
-
.custom(geist, size: size)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
static func geistMonoBold(_ size: CGFloat = 11) -> Font {
|
|
62
|
-
Font.custom(geist, size: size).weight(.medium)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
static func pixel(_ size: CGFloat = 14) -> Font {
|
|
66
|
-
.custom(gohu, size: size)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// MARK: - Background
|
|
71
|
-
|
|
72
|
-
struct PanelBackground: View {
|
|
73
|
-
var body: some View {
|
|
74
|
-
Palette.bg
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// MARK: - Reusable modifiers
|
|
79
|
-
|
|
80
|
-
struct GlassCard: ViewModifier {
|
|
81
|
-
var isHovered: Bool = false
|
|
82
|
-
|
|
83
|
-
func body(content: Content) -> some View {
|
|
84
|
-
content
|
|
85
|
-
.background(
|
|
86
|
-
RoundedRectangle(cornerRadius: 5)
|
|
87
|
-
.fill(isHovered ? Palette.surfaceHov : Palette.surface)
|
|
88
|
-
.overlay(
|
|
89
|
-
RoundedRectangle(cornerRadius: 5)
|
|
90
|
-
.strokeBorder(isHovered ? Palette.borderLit : Palette.border, lineWidth: 0.5)
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
struct LiquidGlassCard: ViewModifier {
|
|
97
|
-
func body(content: Content) -> some View {
|
|
98
|
-
content
|
|
99
|
-
.background(
|
|
100
|
-
ZStack {
|
|
101
|
-
// Base: translucent dark fill
|
|
102
|
-
RoundedRectangle(cornerRadius: 10)
|
|
103
|
-
.fill(Color.white.opacity(0.04))
|
|
104
|
-
|
|
105
|
-
// Subtle gradient: brighter at top edge for "glass reflection"
|
|
106
|
-
RoundedRectangle(cornerRadius: 10)
|
|
107
|
-
.fill(
|
|
108
|
-
LinearGradient(
|
|
109
|
-
colors: [Color.white.opacity(0.06), Color.clear],
|
|
110
|
-
startPoint: .top,
|
|
111
|
-
endPoint: .center
|
|
112
|
-
)
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
// Border: top-bright, bottom-dark for depth
|
|
116
|
-
RoundedRectangle(cornerRadius: 10)
|
|
117
|
-
.strokeBorder(
|
|
118
|
-
LinearGradient(
|
|
119
|
-
colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
|
|
120
|
-
startPoint: .top,
|
|
121
|
-
endPoint: .bottom
|
|
122
|
-
),
|
|
123
|
-
lineWidth: 0.5
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
)
|
|
127
|
-
.shadow(color: Color.black.opacity(0.2), radius: 8, y: 4)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
struct AngularButton: ViewModifier {
|
|
132
|
-
let color: Color
|
|
133
|
-
var filled: Bool = true
|
|
134
|
-
|
|
135
|
-
func body(content: Content) -> some View {
|
|
136
|
-
content
|
|
137
|
-
.font(Typo.monoBold(10))
|
|
138
|
-
.foregroundColor(filled ? Palette.bg : color)
|
|
139
|
-
.padding(.horizontal, 8)
|
|
140
|
-
.padding(.vertical, 4)
|
|
141
|
-
.background(
|
|
142
|
-
RoundedRectangle(cornerRadius: 3)
|
|
143
|
-
.fill(filled ? color : color.opacity(0.10))
|
|
144
|
-
)
|
|
145
|
-
.overlay(
|
|
146
|
-
RoundedRectangle(cornerRadius: 3)
|
|
147
|
-
.strokeBorder(filled ? Color.clear : color.opacity(0.25), lineWidth: 0.5)
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
extension View {
|
|
153
|
-
func glassCard(hovered: Bool = false) -> some View {
|
|
154
|
-
modifier(GlassCard(isHovered: hovered))
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
func liquidGlass() -> some View {
|
|
158
|
-
modifier(LiquidGlassCard())
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
func angularButton(_ color: Color, filled: Bool = true) -> some View {
|
|
162
|
-
modifier(AngularButton(color: color, filled: filled))
|
|
163
|
-
}
|
|
164
|
-
}
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import CoreGraphics
|
|
3
|
-
import AppKit
|
|
4
|
-
|
|
5
|
-
/// Attempt to create stages by simulating drag gestures from the strip.
|
|
6
|
-
///
|
|
7
|
-
/// When a user drags a strip thumbnail into the center stage, SM joins them.
|
|
8
|
-
/// Can we replicate this with synthetic mouse events?
|
|
9
|
-
final class StageDragTests: XCTestCase {
|
|
10
|
-
|
|
11
|
-
// MARK: - Helpers
|
|
12
|
-
|
|
13
|
-
struct LiveWindow {
|
|
14
|
-
let wid: UInt32
|
|
15
|
-
let app: String
|
|
16
|
-
let pid: Int32
|
|
17
|
-
let title: String
|
|
18
|
-
let bounds: CGRect
|
|
19
|
-
let isOnScreen: Bool
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
func getRealWindows() -> [LiveWindow] {
|
|
23
|
-
guard let list = CGWindowListCopyWindowInfo(
|
|
24
|
-
[.optionAll, .excludeDesktopElements],
|
|
25
|
-
kCGNullWindowID
|
|
26
|
-
) as? [[String: Any]] else { return [] }
|
|
27
|
-
|
|
28
|
-
let skip: Set<String> = [
|
|
29
|
-
"Window Server", "Dock", "Control Center", "SystemUIServer",
|
|
30
|
-
"Notification Center", "Spotlight", "WindowManager", "Lattices",
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
return list.compactMap { info in
|
|
34
|
-
guard let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
35
|
-
let owner = info[kCGWindowOwnerName as String] as? String,
|
|
36
|
-
let pid = info[kCGWindowOwnerPID as String] as? Int32,
|
|
37
|
-
let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
|
|
38
|
-
else { return nil }
|
|
39
|
-
|
|
40
|
-
var rect = CGRect.zero
|
|
41
|
-
guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) else { return nil }
|
|
42
|
-
let title = info[kCGWindowName as String] as? String ?? ""
|
|
43
|
-
let layer = info[kCGWindowLayer as String] as? Int ?? 0
|
|
44
|
-
let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? false
|
|
45
|
-
|
|
46
|
-
guard layer == 0, rect.width >= 50, rect.height >= 50 else { return nil }
|
|
47
|
-
guard !skip.contains(owner) else { return nil }
|
|
48
|
-
|
|
49
|
-
return LiveWindow(wid: wid, app: owner, pid: pid, title: title,
|
|
50
|
-
bounds: rect, isOnScreen: isOnScreen)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// Find strip thumbnails (small onscreen windows on the left edge)
|
|
55
|
-
func getStripThumbnails() -> [LiveWindow] {
|
|
56
|
-
getRealWindows().filter {
|
|
57
|
-
$0.isOnScreen && $0.bounds.width < 250 && $0.bounds.height < 250
|
|
58
|
-
&& $0.bounds.origin.x < 220 && $0.bounds.origin.x >= 0
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/// Find active stage windows (large onscreen windows)
|
|
63
|
-
func getActiveStage() -> [LiveWindow] {
|
|
64
|
-
getRealWindows().filter { $0.isOnScreen && $0.bounds.width > 250 }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func printStageState(label: String) {
|
|
68
|
-
let active = getActiveStage()
|
|
69
|
-
let strip = getStripThumbnails()
|
|
70
|
-
print("\n[\(label)]")
|
|
71
|
-
print(" Active: \(active.map { "\($0.app)(\($0.wid))" }.joined(separator: ", "))")
|
|
72
|
-
print(" Strip: \(Set(strip.map(\.app)).sorted().joined(separator: ", "))")
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// MARK: - Synthetic mouse event helpers
|
|
76
|
-
|
|
77
|
-
/// Post a mouse event at a screen coordinate
|
|
78
|
-
func postMouse(_ type: CGEventType, at point: CGPoint, button: CGMouseButton = .left) {
|
|
79
|
-
guard let event = CGEvent(
|
|
80
|
-
mouseEventSource: nil,
|
|
81
|
-
mouseType: type,
|
|
82
|
-
mouseCursorPosition: point,
|
|
83
|
-
mouseButton: button
|
|
84
|
-
) else { return }
|
|
85
|
-
event.post(tap: .cghidEventTap)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Simulate a smooth drag from point A to point B
|
|
89
|
-
func simulateDrag(from start: CGPoint, to end: CGPoint, steps: Int = 30, duration: TimeInterval = 0.4) {
|
|
90
|
-
// Mouse down at start
|
|
91
|
-
postMouse(.leftMouseDown, at: start)
|
|
92
|
-
Thread.sleep(forTimeInterval: 0.05)
|
|
93
|
-
|
|
94
|
-
// Interpolate drag path
|
|
95
|
-
for i in 1...steps {
|
|
96
|
-
let t = CGFloat(i) / CGFloat(steps)
|
|
97
|
-
let x = start.x + (end.x - start.x) * t
|
|
98
|
-
let y = start.y + (end.y - start.y) * t
|
|
99
|
-
postMouse(.leftMouseDragged, at: CGPoint(x: x, y: y))
|
|
100
|
-
Thread.sleep(forTimeInterval: duration / TimeInterval(steps))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Mouse up at end
|
|
104
|
-
postMouse(.leftMouseUp, at: end)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// MARK: - Approach 5: Drag strip thumbnail to center
|
|
108
|
-
|
|
109
|
-
func testJoinByDragFromStrip() throws {
|
|
110
|
-
let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
|
|
111
|
-
try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
|
|
112
|
-
|
|
113
|
-
let thumbnails = getStripThumbnails()
|
|
114
|
-
let active = getActiveStage()
|
|
115
|
-
|
|
116
|
-
guard !thumbnails.isEmpty else {
|
|
117
|
-
print("No strip thumbnails found")
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
guard let anchor = active.first else {
|
|
121
|
-
print("No active stage window")
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Target specific apps: Chrome, iTerm2, Vox
|
|
126
|
-
let activeApps = Set(active.map(\.app))
|
|
127
|
-
let preferred = ["Google Chrome", "iTerm2", "Vox"]
|
|
128
|
-
guard let thumb = thumbnails.first(where: { preferred.contains($0.app) && !activeApps.contains($0.app) })
|
|
129
|
-
?? thumbnails.first(where: { !activeApps.contains($0.app) }) else {
|
|
130
|
-
print("No suitable strip thumbnail found")
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let thumbCenter = CGPoint(x: thumb.bounds.midX, y: thumb.bounds.midY)
|
|
135
|
-
let stageCenter = CGPoint(x: anchor.bounds.midX, y: anchor.bounds.midY)
|
|
136
|
-
|
|
137
|
-
print("Dragging \(thumb.app) thumbnail from strip (\(Int(thumbCenter.x)),\(Int(thumbCenter.y))) to center (\(Int(stageCenter.x)),\(Int(stageCenter.y)))")
|
|
138
|
-
printStageState(label: "BEFORE")
|
|
139
|
-
|
|
140
|
-
simulateDrag(from: thumbCenter, to: stageCenter, steps: 40, duration: 0.6)
|
|
141
|
-
|
|
142
|
-
Thread.sleep(forTimeInterval: 0.8)
|
|
143
|
-
printStageState(label: "After drag to center")
|
|
144
|
-
|
|
145
|
-
let finalActive = getActiveStage()
|
|
146
|
-
let finalApps = Set(finalActive.map(\.app))
|
|
147
|
-
let joined = finalApps.contains(thumb.app) && activeApps.isSubset(of: finalApps)
|
|
148
|
-
print("\nResult: \(thumb.app) joined stage? \(joined)")
|
|
149
|
-
print("Active apps: \(finalApps.sorted())")
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// MARK: - Approach 6: Drag thumbnail to top half (join zone)
|
|
153
|
-
|
|
154
|
-
func testJoinByDragToTopHalf() throws {
|
|
155
|
-
let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
|
|
156
|
-
try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
|
|
157
|
-
|
|
158
|
-
let thumbnails = getStripThumbnails()
|
|
159
|
-
let active = getActiveStage()
|
|
160
|
-
|
|
161
|
-
guard !thumbnails.isEmpty, let anchor = active.first else { return }
|
|
162
|
-
|
|
163
|
-
let activeApps = Set(active.map(\.app))
|
|
164
|
-
let preferred6 = ["Google Chrome", "iTerm2", "Vox"]
|
|
165
|
-
guard let thumb = thumbnails.first(where: { preferred6.contains($0.app) && !activeApps.contains($0.app) })
|
|
166
|
-
?? thumbnails.first(where: { !activeApps.contains($0.app) }) else { return }
|
|
167
|
-
|
|
168
|
-
// SM has specific drop zones. Try dragging to the top half of the active
|
|
169
|
-
// stage area — this might be the "join" zone vs "replace" zone.
|
|
170
|
-
let thumbCenter = CGPoint(x: thumb.bounds.midX, y: thumb.bounds.midY)
|
|
171
|
-
let topHalf = CGPoint(x: anchor.bounds.midX, y: anchor.bounds.origin.y + 100)
|
|
172
|
-
|
|
173
|
-
print("Dragging \(thumb.app) to top-half of active area (\(Int(topHalf.x)),\(Int(topHalf.y)))")
|
|
174
|
-
printStageState(label: "BEFORE")
|
|
175
|
-
|
|
176
|
-
simulateDrag(from: thumbCenter, to: topHalf, steps: 50, duration: 0.8)
|
|
177
|
-
|
|
178
|
-
Thread.sleep(forTimeInterval: 0.8)
|
|
179
|
-
printStageState(label: "After drag to top half")
|
|
180
|
-
|
|
181
|
-
let finalApps = Set(getActiveStage().map(\.app))
|
|
182
|
-
let joined = finalApps.contains(thumb.app) && activeApps.isSubset(of: finalApps)
|
|
183
|
-
print("\nResult: joined? \(joined) — active: \(finalApps.sorted())")
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// MARK: - Approach 7: Long press on thumbnail, then drag
|
|
187
|
-
|
|
188
|
-
func testJoinByLongPressDrag() throws {
|
|
189
|
-
let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
|
|
190
|
-
try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
|
|
191
|
-
|
|
192
|
-
let thumbnails = getStripThumbnails()
|
|
193
|
-
let active = getActiveStage()
|
|
194
|
-
|
|
195
|
-
guard !thumbnails.isEmpty, let anchor = active.first else { return }
|
|
196
|
-
|
|
197
|
-
let activeApps = Set(active.map(\.app))
|
|
198
|
-
let preferred7 = ["Google Chrome", "iTerm2", "Vox"]
|
|
199
|
-
guard let thumb = thumbnails.first(where: { preferred7.contains($0.app) && !activeApps.contains($0.app) })
|
|
200
|
-
?? thumbnails.first(where: { !activeApps.contains($0.app) }) else { return }
|
|
201
|
-
|
|
202
|
-
let thumbCenter = CGPoint(x: thumb.bounds.midX, y: thumb.bounds.midY)
|
|
203
|
-
let stageCenter = CGPoint(x: anchor.bounds.midX, y: anchor.bounds.midY)
|
|
204
|
-
|
|
205
|
-
print("Long-press + drag \(thumb.app) from strip to center")
|
|
206
|
-
printStageState(label: "BEFORE")
|
|
207
|
-
|
|
208
|
-
// Long press: mouse down + wait
|
|
209
|
-
postMouse(.leftMouseDown, at: thumbCenter)
|
|
210
|
-
Thread.sleep(forTimeInterval: 0.8) // Hold for long press recognition
|
|
211
|
-
|
|
212
|
-
// Slow drag out of strip area
|
|
213
|
-
let steps = 60
|
|
214
|
-
let duration: TimeInterval = 1.0
|
|
215
|
-
for i in 1...steps {
|
|
216
|
-
let t = CGFloat(i) / CGFloat(steps)
|
|
217
|
-
let x = thumbCenter.x + (stageCenter.x - thumbCenter.x) * t
|
|
218
|
-
let y = thumbCenter.y + (stageCenter.y - thumbCenter.y) * t
|
|
219
|
-
postMouse(.leftMouseDragged, at: CGPoint(x: x, y: y))
|
|
220
|
-
Thread.sleep(forTimeInterval: duration / TimeInterval(steps))
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Hold at destination briefly
|
|
224
|
-
Thread.sleep(forTimeInterval: 0.3)
|
|
225
|
-
postMouse(.leftMouseUp, at: stageCenter)
|
|
226
|
-
|
|
227
|
-
Thread.sleep(forTimeInterval: 1.0)
|
|
228
|
-
printStageState(label: "After long-press drag")
|
|
229
|
-
|
|
230
|
-
let finalApps = Set(getActiveStage().map(\.app))
|
|
231
|
-
let joined = finalApps.contains(thumb.app) && activeApps.isSubset(of: finalApps)
|
|
232
|
-
print("\nResult: joined? \(joined) — active: \(finalApps.sorted())")
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// MARK: - Approach 8: Click thumbnail while holding Option key
|
|
236
|
-
|
|
237
|
-
func testJoinByOptionClick() throws {
|
|
238
|
-
let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
|
|
239
|
-
try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
|
|
240
|
-
|
|
241
|
-
let thumbnails = getStripThumbnails()
|
|
242
|
-
let active = getActiveStage()
|
|
243
|
-
|
|
244
|
-
guard !thumbnails.isEmpty else { return }
|
|
245
|
-
|
|
246
|
-
let activeApps = Set(active.map(\.app))
|
|
247
|
-
let preferred8 = ["Google Chrome", "iTerm2", "Vox"]
|
|
248
|
-
guard let thumb = thumbnails.first(where: { preferred8.contains($0.app) && !activeApps.contains($0.app) })
|
|
249
|
-
?? thumbnails.first(where: { !activeApps.contains($0.app) }) else { return }
|
|
250
|
-
|
|
251
|
-
let thumbCenter = CGPoint(x: thumb.bounds.midX, y: thumb.bounds.midY)
|
|
252
|
-
|
|
253
|
-
print("Option-clicking \(thumb.app) thumbnail at (\(Int(thumbCenter.x)),\(Int(thumbCenter.y)))")
|
|
254
|
-
printStageState(label: "BEFORE")
|
|
255
|
-
|
|
256
|
-
// Option + click on strip thumbnail
|
|
257
|
-
guard let downEvent = CGEvent(
|
|
258
|
-
mouseEventSource: nil,
|
|
259
|
-
mouseType: .leftMouseDown,
|
|
260
|
-
mouseCursorPosition: thumbCenter,
|
|
261
|
-
mouseButton: .left
|
|
262
|
-
) else { return }
|
|
263
|
-
downEvent.flags = .maskAlternate // Option key
|
|
264
|
-
downEvent.post(tap: .cghidEventTap)
|
|
265
|
-
|
|
266
|
-
Thread.sleep(forTimeInterval: 0.05)
|
|
267
|
-
|
|
268
|
-
guard let upEvent = CGEvent(
|
|
269
|
-
mouseEventSource: nil,
|
|
270
|
-
mouseType: .leftMouseUp,
|
|
271
|
-
mouseCursorPosition: thumbCenter,
|
|
272
|
-
mouseButton: .left
|
|
273
|
-
) else { return }
|
|
274
|
-
upEvent.flags = .maskAlternate
|
|
275
|
-
upEvent.post(tap: .cghidEventTap)
|
|
276
|
-
|
|
277
|
-
Thread.sleep(forTimeInterval: 0.8)
|
|
278
|
-
printStageState(label: "After Option-click")
|
|
279
|
-
|
|
280
|
-
let finalApps = Set(getActiveStage().map(\.app))
|
|
281
|
-
let joined = finalApps.contains(thumb.app) && activeApps.isSubset(of: finalApps)
|
|
282
|
-
print("\nResult: joined? \(joined) — active: \(finalApps.sorted())")
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// MARK: - Approach 9: Shift-click (common modifier for "add to selection")
|
|
286
|
-
|
|
287
|
-
func testJoinByShiftClick() throws {
|
|
288
|
-
let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
|
|
289
|
-
try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
|
|
290
|
-
|
|
291
|
-
let thumbnails = getStripThumbnails()
|
|
292
|
-
let active = getActiveStage()
|
|
293
|
-
|
|
294
|
-
guard !thumbnails.isEmpty else { return }
|
|
295
|
-
|
|
296
|
-
let activeApps = Set(active.map(\.app))
|
|
297
|
-
let preferred9 = ["Google Chrome", "iTerm2", "Vox"]
|
|
298
|
-
guard let thumb = thumbnails.first(where: { preferred9.contains($0.app) && !activeApps.contains($0.app) })
|
|
299
|
-
?? thumbnails.first(where: { !activeApps.contains($0.app) }) else { return }
|
|
300
|
-
|
|
301
|
-
let thumbCenter = CGPoint(x: thumb.bounds.midX, y: thumb.bounds.midY)
|
|
302
|
-
|
|
303
|
-
print("Shift-clicking \(thumb.app) thumbnail")
|
|
304
|
-
printStageState(label: "BEFORE")
|
|
305
|
-
|
|
306
|
-
guard let downEvent = CGEvent(
|
|
307
|
-
mouseEventSource: nil,
|
|
308
|
-
mouseType: .leftMouseDown,
|
|
309
|
-
mouseCursorPosition: thumbCenter,
|
|
310
|
-
mouseButton: .left
|
|
311
|
-
) else { return }
|
|
312
|
-
downEvent.flags = .maskShift
|
|
313
|
-
downEvent.post(tap: .cghidEventTap)
|
|
314
|
-
|
|
315
|
-
Thread.sleep(forTimeInterval: 0.05)
|
|
316
|
-
|
|
317
|
-
guard let upEvent = CGEvent(
|
|
318
|
-
mouseEventSource: nil,
|
|
319
|
-
mouseType: .leftMouseUp,
|
|
320
|
-
mouseCursorPosition: thumbCenter,
|
|
321
|
-
mouseButton: .left
|
|
322
|
-
) else { return }
|
|
323
|
-
upEvent.flags = .maskShift
|
|
324
|
-
upEvent.post(tap: .cghidEventTap)
|
|
325
|
-
|
|
326
|
-
Thread.sleep(forTimeInterval: 0.8)
|
|
327
|
-
printStageState(label: "After Shift-click")
|
|
328
|
-
|
|
329
|
-
let finalApps = Set(getActiveStage().map(\.app))
|
|
330
|
-
let joined = finalApps.contains(thumb.app) && activeApps.isSubset(of: finalApps)
|
|
331
|
-
print("\nResult: joined? \(joined) — active: \(finalApps.sorted())")
|
|
332
|
-
}
|
|
333
|
-
}
|