@lattices/cli 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
|
|
4
|
+
private enum SpotlightConfig {
|
|
5
|
+
static let overlayAlpha: CGFloat = 0.75
|
|
6
|
+
static let dimAlpha: CGFloat = 0.85
|
|
7
|
+
static let spotlightRadius: CGFloat = 200
|
|
8
|
+
static let sonarDelay: TimeInterval = 1.0
|
|
9
|
+
static let totalDuration: TimeInterval = 2.5
|
|
10
|
+
static let fadeInDuration: TimeInterval = 0.15
|
|
11
|
+
static let fadeOutDuration: TimeInterval = 0.4
|
|
12
|
+
static let accentColor = NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: 1.0)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private struct DotMatrixConfig {
|
|
16
|
+
var dotRadius: CGFloat = 2.2
|
|
17
|
+
var dotSpacing: CGFloat = 6.0
|
|
18
|
+
var arrowCols: Int = 13
|
|
19
|
+
var arrowRows: Int = 7 // must be odd
|
|
20
|
+
|
|
21
|
+
static let shared: DotMatrixConfig = {
|
|
22
|
+
let path = NSHomeDirectory() + "/.lattices/mouse-finder.json"
|
|
23
|
+
guard FileManager.default.fileExists(atPath: path),
|
|
24
|
+
let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
|
|
25
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
26
|
+
else { return DotMatrixConfig() }
|
|
27
|
+
|
|
28
|
+
var config = DotMatrixConfig()
|
|
29
|
+
if let v = json["dotRadius"] as? Double { config.dotRadius = CGFloat(v) }
|
|
30
|
+
if let v = json["dotSpacing"] as? Double { config.dotSpacing = CGFloat(v) }
|
|
31
|
+
if let v = json["arrowCols"] as? Int { config.arrowCols = max(3, v) }
|
|
32
|
+
if let v = json["arrowRows"] as? Int { config.arrowRows = max(3, v | 1) }
|
|
33
|
+
return config
|
|
34
|
+
}()
|
|
35
|
+
|
|
36
|
+
func generatePattern() -> [(col: Int, row: Int)] {
|
|
37
|
+
let center = arrowRows / 2
|
|
38
|
+
let shaftHalf = center / 2
|
|
39
|
+
var dots: [(Int, Int)] = []
|
|
40
|
+
|
|
41
|
+
for r in 0..<arrowRows {
|
|
42
|
+
let d = abs(r - center)
|
|
43
|
+
if d <= shaftHalf {
|
|
44
|
+
for c in 0...(arrowCols - 1 - d) { dots.append((c, r)) }
|
|
45
|
+
} else {
|
|
46
|
+
let headTip = arrowCols - 1 - d
|
|
47
|
+
let headStart = max(0, headTip - 1)
|
|
48
|
+
for c in headStart...headTip { dots.append((c, r)) }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return dots
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Locates the mouse cursor with a spotlight + sonar pulse effect.
|
|
56
|
+
/// Dims all screens, spotlights the cursor area, shows directional arrows on off-screens,
|
|
57
|
+
/// then plays sonar rings on top.
|
|
58
|
+
final class MouseFinder {
|
|
59
|
+
static let shared = MouseFinder()
|
|
60
|
+
|
|
61
|
+
private var overlayWindows: [NSWindow] = []
|
|
62
|
+
private var sonarWindows: [NSWindow] = []
|
|
63
|
+
private var dismissTimer: Timer?
|
|
64
|
+
private var animationTimer: Timer?
|
|
65
|
+
private var sonarDelayTimer: Timer?
|
|
66
|
+
private var animationStart: CFTimeInterval = 0
|
|
67
|
+
private let animationDuration: CFTimeInterval = 1.5
|
|
68
|
+
private var globalEventMonitor: Any?
|
|
69
|
+
private var localEventMonitor: Any?
|
|
70
|
+
|
|
71
|
+
// MARK: - Find (highlight current position)
|
|
72
|
+
|
|
73
|
+
func find() {
|
|
74
|
+
let pos = NSEvent.mouseLocation
|
|
75
|
+
showSpotlight(at: pos)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MARK: - Summon (warp to center of the screen the mouse is on, or a specific point)
|
|
79
|
+
|
|
80
|
+
func summon(to point: CGPoint? = nil) {
|
|
81
|
+
let target: NSPoint
|
|
82
|
+
if let point {
|
|
83
|
+
target = point
|
|
84
|
+
} else {
|
|
85
|
+
let screen = mouseScreen()
|
|
86
|
+
let frame = screen.frame
|
|
87
|
+
target = NSPoint(x: frame.midX, y: frame.midY)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
91
|
+
let cgPoint = CGPoint(x: target.x, y: primaryHeight - target.y)
|
|
92
|
+
CGWarpMouseCursorPosition(cgPoint)
|
|
93
|
+
CGAssociateMouseAndMouseCursorPosition(1)
|
|
94
|
+
|
|
95
|
+
showSpotlight(at: target, mode: .summon)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Spotlight Effect
|
|
99
|
+
|
|
100
|
+
private func showSpotlight(at nsPoint: NSPoint, mode: SpotlightMode = .find) {
|
|
101
|
+
dismiss()
|
|
102
|
+
|
|
103
|
+
let screens = NSScreen.screens
|
|
104
|
+
guard !screens.isEmpty else { return }
|
|
105
|
+
|
|
106
|
+
let cursorScreen = screens.first(where: { $0.frame.contains(nsPoint) }) ?? screens[0]
|
|
107
|
+
let otherScreens = screens.filter { $0 !== cursorScreen }
|
|
108
|
+
let windowLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
|
|
109
|
+
|
|
110
|
+
// Spotlight overlay on cursor screen
|
|
111
|
+
let localCursor = NSPoint(
|
|
112
|
+
x: nsPoint.x - cursorScreen.frame.origin.x,
|
|
113
|
+
y: nsPoint.y - cursorScreen.frame.origin.y
|
|
114
|
+
)
|
|
115
|
+
let spotlightWindow = makeOverlayWindow(frame: cursorScreen.frame, level: windowLevel)
|
|
116
|
+
spotlightWindow.contentView = SpotlightView(
|
|
117
|
+
frame: NSRect(origin: .zero, size: cursorScreen.frame.size),
|
|
118
|
+
cursorPoint: localCursor,
|
|
119
|
+
mode: mode
|
|
120
|
+
)
|
|
121
|
+
overlayWindows.append(spotlightWindow)
|
|
122
|
+
|
|
123
|
+
// Dim overlays with directional arrows on other screens
|
|
124
|
+
for screen in otherScreens {
|
|
125
|
+
let screenCenter = NSPoint(
|
|
126
|
+
x: screen.frame.midX,
|
|
127
|
+
y: screen.frame.midY
|
|
128
|
+
)
|
|
129
|
+
let angle = atan2(nsPoint.y - screenCenter.y, nsPoint.x - screenCenter.x)
|
|
130
|
+
|
|
131
|
+
let dimWindow = makeOverlayWindow(frame: screen.frame, level: windowLevel)
|
|
132
|
+
dimWindow.contentView = DimOverlayView(
|
|
133
|
+
frame: NSRect(origin: .zero, size: screen.frame.size),
|
|
134
|
+
cursorAngle: angle
|
|
135
|
+
)
|
|
136
|
+
overlayWindows.append(dimWindow)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fade all in
|
|
140
|
+
for window in overlayWindows {
|
|
141
|
+
window.alphaValue = 0
|
|
142
|
+
window.orderFrontRegardless()
|
|
143
|
+
NSAnimationContext.runAnimationGroup { ctx in
|
|
144
|
+
ctx.duration = SpotlightConfig.fadeInDuration
|
|
145
|
+
window.animator().alphaValue = 1.0
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
installEventMonitors()
|
|
150
|
+
|
|
151
|
+
// Start sonar after delay
|
|
152
|
+
sonarDelayTimer = Timer.scheduledTimer(withTimeInterval: SpotlightConfig.sonarDelay, repeats: false) { [weak self] _ in
|
|
153
|
+
self?.showSonar(at: nsPoint)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Auto-dismiss
|
|
157
|
+
dismissTimer = Timer.scheduledTimer(withTimeInterval: SpotlightConfig.totalDuration, repeats: false) { [weak self] _ in
|
|
158
|
+
self?.fadeOut()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// MARK: - Sonar Animation (plays on top of spotlight)
|
|
163
|
+
|
|
164
|
+
private func showSonar(at nsPoint: NSPoint) {
|
|
165
|
+
let screens = NSScreen.screens
|
|
166
|
+
guard !screens.isEmpty else { return }
|
|
167
|
+
|
|
168
|
+
let ringCount = 3
|
|
169
|
+
let maxRadius: CGFloat = 120
|
|
170
|
+
let totalSize = maxRadius * 2 + 20
|
|
171
|
+
let sonarLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)) + 1)
|
|
172
|
+
|
|
173
|
+
for screen in screens {
|
|
174
|
+
let extendedBounds = screen.frame.insetBy(dx: -maxRadius, dy: -maxRadius)
|
|
175
|
+
guard extendedBounds.contains(nsPoint) else { continue }
|
|
176
|
+
|
|
177
|
+
let windowFrame = NSRect(
|
|
178
|
+
x: nsPoint.x - totalSize / 2,
|
|
179
|
+
y: nsPoint.y - totalSize / 2,
|
|
180
|
+
width: totalSize,
|
|
181
|
+
height: totalSize
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
let window = NSWindow(
|
|
185
|
+
contentRect: windowFrame,
|
|
186
|
+
styleMask: .borderless,
|
|
187
|
+
backing: .buffered,
|
|
188
|
+
defer: false
|
|
189
|
+
)
|
|
190
|
+
window.isOpaque = false
|
|
191
|
+
window.backgroundColor = .clear
|
|
192
|
+
window.level = sonarLevel
|
|
193
|
+
window.hasShadow = false
|
|
194
|
+
window.ignoresMouseEvents = true
|
|
195
|
+
window.collectionBehavior = [.canJoinAllSpaces, .stationary]
|
|
196
|
+
|
|
197
|
+
let sonarView = SonarView(
|
|
198
|
+
frame: NSRect(origin: .zero, size: windowFrame.size),
|
|
199
|
+
ringCount: ringCount,
|
|
200
|
+
maxRadius: maxRadius
|
|
201
|
+
)
|
|
202
|
+
window.contentView = sonarView
|
|
203
|
+
|
|
204
|
+
window.alphaValue = 0
|
|
205
|
+
window.orderFrontRegardless()
|
|
206
|
+
sonarWindows.append(window)
|
|
207
|
+
|
|
208
|
+
NSAnimationContext.runAnimationGroup { ctx in
|
|
209
|
+
ctx.duration = 0.1
|
|
210
|
+
window.animator().alphaValue = 1.0
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
animationStart = CACurrentMediaTime()
|
|
215
|
+
let interval = 1.0 / 60.0
|
|
216
|
+
|
|
217
|
+
animationTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] timer in
|
|
218
|
+
guard let self else { timer.invalidate(); return }
|
|
219
|
+
let elapsed = CACurrentMediaTime() - self.animationStart
|
|
220
|
+
let progress = CGFloat(min(elapsed / self.animationDuration, 1.0))
|
|
221
|
+
|
|
222
|
+
for window in self.sonarWindows {
|
|
223
|
+
(window.contentView as? SonarView)?.progress = progress
|
|
224
|
+
window.contentView?.needsDisplay = true
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if progress >= 1.0 {
|
|
228
|
+
timer.invalidate()
|
|
229
|
+
self.animationTimer = nil
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// MARK: - Lifecycle
|
|
235
|
+
|
|
236
|
+
private func fadeOut() {
|
|
237
|
+
let allWindows = overlayWindows + sonarWindows
|
|
238
|
+
NSAnimationContext.runAnimationGroup({ ctx in
|
|
239
|
+
ctx.duration = SpotlightConfig.fadeOutDuration
|
|
240
|
+
for window in allWindows {
|
|
241
|
+
window.animator().alphaValue = 0
|
|
242
|
+
}
|
|
243
|
+
}, completionHandler: { [weak self] in
|
|
244
|
+
self?.dismiss()
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func dismiss() {
|
|
249
|
+
removeEventMonitors()
|
|
250
|
+
animationTimer?.invalidate()
|
|
251
|
+
animationTimer = nil
|
|
252
|
+
dismissTimer?.invalidate()
|
|
253
|
+
dismissTimer = nil
|
|
254
|
+
sonarDelayTimer?.invalidate()
|
|
255
|
+
sonarDelayTimer = nil
|
|
256
|
+
for window in overlayWindows + sonarWindows {
|
|
257
|
+
window.orderOut(nil)
|
|
258
|
+
}
|
|
259
|
+
overlayWindows.removeAll()
|
|
260
|
+
sonarWindows.removeAll()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// MARK: - Event Monitors
|
|
264
|
+
|
|
265
|
+
private func installEventMonitors() {
|
|
266
|
+
globalEventMonitor = NSEvent.addGlobalMonitorForEvents(
|
|
267
|
+
matching: [.leftMouseDown, .rightMouseDown, .keyDown]
|
|
268
|
+
) { [weak self] _ in
|
|
269
|
+
self?.dismiss()
|
|
270
|
+
}
|
|
271
|
+
localEventMonitor = NSEvent.addLocalMonitorForEvents(
|
|
272
|
+
matching: [.leftMouseDown, .rightMouseDown, .keyDown]
|
|
273
|
+
) { [weak self] event in
|
|
274
|
+
self?.dismiss()
|
|
275
|
+
return event
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private func removeEventMonitors() {
|
|
280
|
+
if let m = globalEventMonitor { NSEvent.removeMonitor(m); globalEventMonitor = nil }
|
|
281
|
+
if let m = localEventMonitor { NSEvent.removeMonitor(m); localEventMonitor = nil }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// MARK: - Helpers
|
|
285
|
+
|
|
286
|
+
private func makeOverlayWindow(frame: NSRect, level: NSWindow.Level) -> NSWindow {
|
|
287
|
+
let window = NSWindow(
|
|
288
|
+
contentRect: frame,
|
|
289
|
+
styleMask: .borderless,
|
|
290
|
+
backing: .buffered,
|
|
291
|
+
defer: false
|
|
292
|
+
)
|
|
293
|
+
window.isOpaque = false
|
|
294
|
+
window.backgroundColor = .clear
|
|
295
|
+
window.level = level
|
|
296
|
+
window.hasShadow = false
|
|
297
|
+
window.ignoresMouseEvents = true
|
|
298
|
+
window.collectionBehavior = [.canJoinAllSpaces, .stationary]
|
|
299
|
+
return window
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func mouseScreen() -> NSScreen {
|
|
303
|
+
let pos = NSEvent.mouseLocation
|
|
304
|
+
return NSScreen.screens.first(where: { $0.frame.contains(pos) }) ?? NSScreen.screens[0]
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// MARK: - Spotlight View (radial gradient cutout on cursor screen)
|
|
309
|
+
|
|
310
|
+
enum SpotlightMode {
|
|
311
|
+
case find // single arrow at screen center pointing TO the cursor
|
|
312
|
+
case summon // four arrows around the cursor pointing INWARD ("conjured here")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private class SpotlightView: NSView {
|
|
316
|
+
let cursorPoint: CGPoint
|
|
317
|
+
let mode: SpotlightMode
|
|
318
|
+
private let config = DotMatrixConfig.shared
|
|
319
|
+
private lazy var dotPattern = config.generatePattern()
|
|
320
|
+
|
|
321
|
+
init(frame: NSRect, cursorPoint: CGPoint, mode: SpotlightMode = .find) {
|
|
322
|
+
self.cursorPoint = cursorPoint
|
|
323
|
+
self.mode = mode
|
|
324
|
+
super.init(frame: frame)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
328
|
+
|
|
329
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
330
|
+
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
331
|
+
|
|
332
|
+
ctx.setFillColor(NSColor.black.withAlphaComponent(SpotlightConfig.overlayAlpha).cgColor)
|
|
333
|
+
ctx.fill(bounds)
|
|
334
|
+
|
|
335
|
+
// Punch a radial gradient hole using destinationOut blend mode
|
|
336
|
+
ctx.setBlendMode(.destinationOut)
|
|
337
|
+
|
|
338
|
+
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
339
|
+
let components: [CGFloat] = [
|
|
340
|
+
1, 1, 1, 1.0,
|
|
341
|
+
1, 1, 1, 0.8,
|
|
342
|
+
1, 1, 1, 0.0,
|
|
343
|
+
]
|
|
344
|
+
let locations: [CGFloat] = [0.0, 0.3, 1.0]
|
|
345
|
+
|
|
346
|
+
guard let gradient = CGGradient(
|
|
347
|
+
colorSpace: colorSpace,
|
|
348
|
+
colorComponents: components,
|
|
349
|
+
locations: locations,
|
|
350
|
+
count: 3
|
|
351
|
+
) else { return }
|
|
352
|
+
|
|
353
|
+
ctx.drawRadialGradient(
|
|
354
|
+
gradient,
|
|
355
|
+
startCenter: cursorPoint,
|
|
356
|
+
startRadius: 0,
|
|
357
|
+
endCenter: cursorPoint,
|
|
358
|
+
endRadius: SpotlightConfig.spotlightRadius,
|
|
359
|
+
options: []
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
ctx.setBlendMode(.normal)
|
|
363
|
+
|
|
364
|
+
switch mode {
|
|
365
|
+
case .find:
|
|
366
|
+
// Single arrow at screen center pointing toward the cursor.
|
|
367
|
+
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
368
|
+
let angle = atan2(cursorPoint.y - center.y, cursorPoint.x - center.x)
|
|
369
|
+
drawDotMatrixArrow(in: ctx, at: center, angle: angle)
|
|
370
|
+
|
|
371
|
+
case .summon:
|
|
372
|
+
// Four arrows around the cursor, all heads pointing inward toward it —
|
|
373
|
+
// the visual joke is that the mouse was just summoned here, so everything
|
|
374
|
+
// is converging on the new cursor position.
|
|
375
|
+
let arrowLen = CGFloat(config.arrowCols - 1) * config.dotSpacing
|
|
376
|
+
let offset = arrowLen / 2 + SpotlightConfig.spotlightRadius * 0.55
|
|
377
|
+
let placements: [(CGPoint, CGFloat)] = [
|
|
378
|
+
(CGPoint(x: cursorPoint.x, y: cursorPoint.y + offset), -.pi / 2), // above → points down
|
|
379
|
+
(CGPoint(x: cursorPoint.x, y: cursorPoint.y - offset), .pi / 2), // below → points up
|
|
380
|
+
(CGPoint(x: cursorPoint.x - offset, y: cursorPoint.y), 0), // left → points right
|
|
381
|
+
(CGPoint(x: cursorPoint.x + offset, y: cursorPoint.y), .pi), // right → points left
|
|
382
|
+
]
|
|
383
|
+
for (origin, angle) in placements {
|
|
384
|
+
drawDotMatrixArrow(in: ctx, at: origin, angle: angle)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private func drawDotMatrixArrow(in ctx: CGContext, at point: CGPoint, angle: CGFloat) {
|
|
390
|
+
ctx.saveGState()
|
|
391
|
+
ctx.translateBy(x: point.x, y: point.y)
|
|
392
|
+
ctx.rotate(by: angle)
|
|
393
|
+
|
|
394
|
+
let originX = -CGFloat(config.arrowCols - 1) * config.dotSpacing / 2
|
|
395
|
+
let originY = -CGFloat(config.arrowRows - 1) * config.dotSpacing / 2
|
|
396
|
+
|
|
397
|
+
for (col, row) in dotPattern {
|
|
398
|
+
let x = originX + CGFloat(col) * config.dotSpacing
|
|
399
|
+
let y = originY + CGFloat(row) * config.dotSpacing
|
|
400
|
+
|
|
401
|
+
let t = CGFloat(col) / CGFloat(max(1, config.arrowCols - 1))
|
|
402
|
+
let alpha = 0.35 + t * 0.5
|
|
403
|
+
|
|
404
|
+
ctx.setFillColor(SpotlightConfig.accentColor.withAlphaComponent(alpha).cgColor)
|
|
405
|
+
ctx.fillEllipse(in: CGRect(
|
|
406
|
+
x: x - config.dotRadius,
|
|
407
|
+
y: y - config.dotRadius,
|
|
408
|
+
width: config.dotRadius * 2,
|
|
409
|
+
height: config.dotRadius * 2
|
|
410
|
+
))
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
ctx.restoreGState()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// MARK: - Dim Overlay View (dark fill + dot matrix arrow centered on off-screens)
|
|
418
|
+
|
|
419
|
+
private class DimOverlayView: NSView {
|
|
420
|
+
let cursorAngle: CGFloat
|
|
421
|
+
private let config = DotMatrixConfig.shared
|
|
422
|
+
private lazy var dotPattern = config.generatePattern()
|
|
423
|
+
|
|
424
|
+
init(frame: NSRect, cursorAngle: CGFloat) {
|
|
425
|
+
self.cursorAngle = cursorAngle
|
|
426
|
+
super.init(frame: frame)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
430
|
+
|
|
431
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
432
|
+
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
433
|
+
|
|
434
|
+
ctx.setFillColor(NSColor.black.withAlphaComponent(SpotlightConfig.dimAlpha).cgColor)
|
|
435
|
+
ctx.fill(bounds)
|
|
436
|
+
|
|
437
|
+
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
438
|
+
|
|
439
|
+
ctx.saveGState()
|
|
440
|
+
ctx.translateBy(x: center.x, y: center.y)
|
|
441
|
+
ctx.rotate(by: cursorAngle)
|
|
442
|
+
|
|
443
|
+
let originX = -CGFloat(config.arrowCols - 1) * config.dotSpacing / 2
|
|
444
|
+
let originY = -CGFloat(config.arrowRows - 1) * config.dotSpacing / 2
|
|
445
|
+
|
|
446
|
+
for (col, row) in dotPattern {
|
|
447
|
+
let x = originX + CGFloat(col) * config.dotSpacing
|
|
448
|
+
let y = originY + CGFloat(row) * config.dotSpacing
|
|
449
|
+
|
|
450
|
+
let t = CGFloat(col) / CGFloat(max(1, config.arrowCols - 1))
|
|
451
|
+
let alpha = 0.35 + t * 0.5
|
|
452
|
+
|
|
453
|
+
ctx.setFillColor(SpotlightConfig.accentColor.withAlphaComponent(alpha).cgColor)
|
|
454
|
+
ctx.fillEllipse(in: CGRect(
|
|
455
|
+
x: x - config.dotRadius,
|
|
456
|
+
y: y - config.dotRadius,
|
|
457
|
+
width: config.dotRadius * 2,
|
|
458
|
+
height: config.dotRadius * 2
|
|
459
|
+
))
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
ctx.restoreGState()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// MARK: - Sonar Ring View
|
|
467
|
+
|
|
468
|
+
private class SonarView: NSView {
|
|
469
|
+
let ringCount: Int
|
|
470
|
+
let maxRadius: CGFloat
|
|
471
|
+
var progress: CGFloat = 0
|
|
472
|
+
|
|
473
|
+
init(frame: NSRect, ringCount: Int, maxRadius: CGFloat) {
|
|
474
|
+
self.ringCount = ringCount
|
|
475
|
+
self.maxRadius = maxRadius
|
|
476
|
+
super.init(frame: frame)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
480
|
+
|
|
481
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
482
|
+
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
483
|
+
|
|
484
|
+
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
485
|
+
|
|
486
|
+
for i in 0..<ringCount {
|
|
487
|
+
let ringDelay = CGFloat(i) * 0.15
|
|
488
|
+
let denom = 1.0 - ringDelay * CGFloat(ringCount - 1) / CGFloat(ringCount)
|
|
489
|
+
let ringProgress = max(0, min(1, (progress - ringDelay) / denom))
|
|
490
|
+
|
|
491
|
+
guard ringProgress > 0 else { continue }
|
|
492
|
+
|
|
493
|
+
let eased = 1.0 - pow(1.0 - ringProgress, 3)
|
|
494
|
+
let radius = maxRadius * eased
|
|
495
|
+
let alpha = (1.0 - eased) * 0.8
|
|
496
|
+
|
|
497
|
+
ctx.setStrokeColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: alpha).cgColor)
|
|
498
|
+
ctx.setLineWidth(2.5 - CGFloat(i) * 0.5)
|
|
499
|
+
ctx.addEllipse(in: CGRect(
|
|
500
|
+
x: center.x - radius,
|
|
501
|
+
y: center.y - radius,
|
|
502
|
+
width: radius * 2,
|
|
503
|
+
height: radius * 2
|
|
504
|
+
))
|
|
505
|
+
ctx.strokePath()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let dotRadius: CGFloat = 6
|
|
509
|
+
let dotAlpha = max(0.3, 1.0 - progress * 0.5)
|
|
510
|
+
ctx.setFillColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: dotAlpha).cgColor)
|
|
511
|
+
ctx.fillEllipse(in: CGRect(
|
|
512
|
+
x: center.x - dotRadius,
|
|
513
|
+
y: center.y - dotRadius,
|
|
514
|
+
width: dotRadius * 2,
|
|
515
|
+
height: dotRadius * 2
|
|
516
|
+
))
|
|
517
|
+
|
|
518
|
+
ctx.setFillColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: dotAlpha * 0.2).cgColor)
|
|
519
|
+
let glowRadius: CGFloat = 12
|
|
520
|
+
ctx.fillEllipse(in: CGRect(
|
|
521
|
+
x: center.x - glowRadius,
|
|
522
|
+
y: center.y - glowRadius,
|
|
523
|
+
width: glowRadius * 2,
|
|
524
|
+
height: glowRadius * 2
|
|
525
|
+
))
|
|
526
|
+
}
|
|
527
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ApplicationServices
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
|
|
5
|
+
struct LocatedWindow {
|
|
6
|
+
let wid: UInt32
|
|
7
|
+
let pid: pid_t
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum SessionWindowLocator {
|
|
11
|
+
static func tag(for session: String) -> String {
|
|
12
|
+
Terminal.windowTag(for: session)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func extractSessionName(from title: String) -> String? {
|
|
16
|
+
guard let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) else {
|
|
17
|
+
return nil
|
|
18
|
+
}
|
|
19
|
+
let match = String(title[range])
|
|
20
|
+
return String(match.dropFirst(10).dropLast(1))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func matches(session: String, title: String, extractedSessionName: String? = nil) -> Bool {
|
|
24
|
+
if extractedSessionName == session {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
return title.contains(tag(for: session))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func cachedWindow(forSession session: String, in windows: [UInt32: WindowEntry]) -> WindowEntry? {
|
|
31
|
+
windows.values.first { entry in
|
|
32
|
+
matches(session: session, title: entry.title, extractedSessionName: entry.latticesSession)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static func cachedWindow(forSession session: String, desktopModel: DesktopModel = .shared) -> WindowEntry? {
|
|
37
|
+
cachedWindow(forSession: session, in: desktopModel.windows)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static func findCGWindow(tag: String) -> LocatedWindow? {
|
|
41
|
+
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for info in windowList {
|
|
46
|
+
if let name = info[kCGWindowName as String] as? String,
|
|
47
|
+
name.contains(tag),
|
|
48
|
+
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
49
|
+
let pid = info[kCGWindowOwnerPID as String] as? pid_t {
|
|
50
|
+
return LocatedWindow(wid: wid, pid: pid)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static func findWindow(session: String, terminal: Terminal) -> LocatedWindow? {
|
|
57
|
+
findWindow(tag: tag(for: session), terminal: terminal)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static func findWindow(tag: String, terminal: Terminal) -> LocatedWindow? {
|
|
61
|
+
if let match = findCGWindow(tag: tag) {
|
|
62
|
+
return match
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if let ax = findAXWindow(terminal: terminal, tag: tag),
|
|
66
|
+
let wid = matchCGWindow(pid: ax.pid, axWindow: ax.window) {
|
|
67
|
+
return LocatedWindow(wid: wid, pid: ax.pid)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static func findAXWindow(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
|
|
74
|
+
let diag = DiagnosticLog.shared
|
|
75
|
+
guard let app = NSWorkspace.shared.runningApplications.first(where: {
|
|
76
|
+
$0.bundleIdentifier == terminal.bundleId
|
|
77
|
+
}) else {
|
|
78
|
+
diag.error("SessionWindowLocator.findAXWindow: \(terminal.rawValue) (\(terminal.bundleId)) not running")
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let pid = app.processIdentifier
|
|
83
|
+
let appRef = AXUIElementCreateApplication(pid)
|
|
84
|
+
var windowsRef: CFTypeRef?
|
|
85
|
+
let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
|
|
86
|
+
guard err == .success, let windows = windowsRef as? [AXUIElement] else {
|
|
87
|
+
diag.error("SessionWindowLocator.findAXWindow: AX error \(err.rawValue) — Accessibility not granted?")
|
|
88
|
+
return nil
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
diag.info("SessionWindowLocator.findAXWindow: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
|
|
92
|
+
for win in windows {
|
|
93
|
+
var titleRef: CFTypeRef?
|
|
94
|
+
AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
|
|
95
|
+
let title = titleRef as? String ?? "<no title>"
|
|
96
|
+
if title.contains(tag) {
|
|
97
|
+
diag.success("SessionWindowLocator.findAXWindow: matched \"\(title)\"")
|
|
98
|
+
return (pid, win)
|
|
99
|
+
} else {
|
|
100
|
+
diag.info(" skip: \"\(title)\"")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
diag.warn("SessionWindowLocator.findAXWindow: no window matched tag \(tag)")
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
|
|
109
|
+
var posRef: CFTypeRef?
|
|
110
|
+
var sizeRef: CFTypeRef?
|
|
111
|
+
AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
|
|
112
|
+
AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
|
|
113
|
+
guard let pv = posRef, let sv = sizeRef else { return nil }
|
|
114
|
+
|
|
115
|
+
var pos = CGPoint.zero
|
|
116
|
+
var size = CGSize.zero
|
|
117
|
+
AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
|
|
118
|
+
AXValueGetValue(sv as! AXValue, .cgSize, &size)
|
|
119
|
+
|
|
120
|
+
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for info in windowList {
|
|
125
|
+
guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
|
|
126
|
+
wPid == pid,
|
|
127
|
+
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
128
|
+
let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
|
|
129
|
+
var rect = CGRect.zero
|
|
130
|
+
if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
|
|
131
|
+
if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
|
|
132
|
+
abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
|
|
133
|
+
return wid
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return nil
|
|
138
|
+
}
|
|
139
|
+
}
|