@lattices/cli 0.4.1 → 0.4.5
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/ActionRow.swift +43 -26
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +91 -30
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +53 -16
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +398 -186
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/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 +65 -1
- 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/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -2
|
@@ -1,23 +1,78 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import CoreGraphics
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
7
58
|
final class MouseFinder {
|
|
8
59
|
static let shared = MouseFinder()
|
|
9
60
|
|
|
10
61
|
private var overlayWindows: [NSWindow] = []
|
|
62
|
+
private var sonarWindows: [NSWindow] = []
|
|
11
63
|
private var dismissTimer: Timer?
|
|
12
64
|
private var animationTimer: Timer?
|
|
65
|
+
private var sonarDelayTimer: Timer?
|
|
13
66
|
private var animationStart: CFTimeInterval = 0
|
|
14
67
|
private let animationDuration: CFTimeInterval = 1.5
|
|
68
|
+
private var globalEventMonitor: Any?
|
|
69
|
+
private var localEventMonitor: Any?
|
|
15
70
|
|
|
16
71
|
// MARK: - Find (highlight current position)
|
|
17
72
|
|
|
18
73
|
func find() {
|
|
19
74
|
let pos = NSEvent.mouseLocation
|
|
20
|
-
|
|
75
|
+
showSpotlight(at: pos)
|
|
21
76
|
}
|
|
22
77
|
|
|
23
78
|
// MARK: - Summon (warp to center of the screen the mouse is on, or a specific point)
|
|
@@ -32,30 +87,90 @@ final class MouseFinder {
|
|
|
32
87
|
target = NSPoint(x: frame.midX, y: frame.midY)
|
|
33
88
|
}
|
|
34
89
|
|
|
35
|
-
// CGWarpMouseCursorPosition uses top-left origin
|
|
36
90
|
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
37
91
|
let cgPoint = CGPoint(x: target.x, y: primaryHeight - target.y)
|
|
38
92
|
CGWarpMouseCursorPosition(cgPoint)
|
|
39
|
-
// Re-associate mouse with cursor position after warp
|
|
40
93
|
CGAssociateMouseAndMouseCursorPosition(1)
|
|
41
94
|
|
|
42
|
-
|
|
95
|
+
showSpotlight(at: target, mode: .summon)
|
|
43
96
|
}
|
|
44
97
|
|
|
45
|
-
// MARK: -
|
|
98
|
+
// MARK: - Spotlight Effect
|
|
46
99
|
|
|
47
|
-
private func
|
|
100
|
+
private func showSpotlight(at nsPoint: NSPoint, mode: SpotlightMode = .find) {
|
|
48
101
|
dismiss()
|
|
49
102
|
|
|
50
103
|
let screens = NSScreen.screens
|
|
51
104
|
guard !screens.isEmpty else { return }
|
|
52
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
|
+
|
|
53
168
|
let ringCount = 3
|
|
54
169
|
let maxRadius: CGFloat = 120
|
|
55
170
|
let totalSize = maxRadius * 2 + 20
|
|
171
|
+
let sonarLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)) + 1)
|
|
56
172
|
|
|
57
173
|
for screen in screens {
|
|
58
|
-
// Only show on screens near the cursor
|
|
59
174
|
let extendedBounds = screen.frame.insetBy(dx: -maxRadius, dy: -maxRadius)
|
|
60
175
|
guard extendedBounds.contains(nsPoint) else { continue }
|
|
61
176
|
|
|
@@ -74,7 +189,7 @@ final class MouseFinder {
|
|
|
74
189
|
)
|
|
75
190
|
window.isOpaque = false
|
|
76
191
|
window.backgroundColor = .clear
|
|
77
|
-
window.level =
|
|
192
|
+
window.level = sonarLevel
|
|
78
193
|
window.hasShadow = false
|
|
79
194
|
window.ignoresMouseEvents = true
|
|
80
195
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary]
|
|
@@ -88,16 +203,14 @@ final class MouseFinder {
|
|
|
88
203
|
|
|
89
204
|
window.alphaValue = 0
|
|
90
205
|
window.orderFrontRegardless()
|
|
91
|
-
|
|
206
|
+
sonarWindows.append(window)
|
|
92
207
|
|
|
93
|
-
// Fade in
|
|
94
208
|
NSAnimationContext.runAnimationGroup { ctx in
|
|
95
209
|
ctx.duration = 0.1
|
|
96
210
|
window.animator().alphaValue = 1.0
|
|
97
211
|
}
|
|
98
212
|
}
|
|
99
213
|
|
|
100
|
-
// Animate the rings expanding using CACurrentMediaTime for state
|
|
101
214
|
animationStart = CACurrentMediaTime()
|
|
102
215
|
let interval = 1.0 / 60.0
|
|
103
216
|
|
|
@@ -106,7 +219,7 @@ final class MouseFinder {
|
|
|
106
219
|
let elapsed = CACurrentMediaTime() - self.animationStart
|
|
107
220
|
let progress = CGFloat(min(elapsed / self.animationDuration, 1.0))
|
|
108
221
|
|
|
109
|
-
for window in self.
|
|
222
|
+
for window in self.sonarWindows {
|
|
110
223
|
(window.contentView as? SonarView)?.progress = progress
|
|
111
224
|
window.contentView?.needsDisplay = true
|
|
112
225
|
}
|
|
@@ -116,18 +229,15 @@ final class MouseFinder {
|
|
|
116
229
|
self.animationTimer = nil
|
|
117
230
|
}
|
|
118
231
|
}
|
|
119
|
-
|
|
120
|
-
// Auto-dismiss after animation + hold
|
|
121
|
-
dismissTimer = Timer.scheduledTimer(withTimeInterval: 2.5, repeats: false) { [weak self] _ in
|
|
122
|
-
self?.fadeOut()
|
|
123
|
-
}
|
|
124
232
|
}
|
|
125
233
|
|
|
234
|
+
// MARK: - Lifecycle
|
|
235
|
+
|
|
126
236
|
private func fadeOut() {
|
|
127
|
-
let
|
|
237
|
+
let allWindows = overlayWindows + sonarWindows
|
|
128
238
|
NSAnimationContext.runAnimationGroup({ ctx in
|
|
129
|
-
ctx.duration =
|
|
130
|
-
for window in
|
|
239
|
+
ctx.duration = SpotlightConfig.fadeOutDuration
|
|
240
|
+
for window in allWindows {
|
|
131
241
|
window.animator().alphaValue = 0
|
|
132
242
|
}
|
|
133
243
|
}, completionHandler: { [weak self] in
|
|
@@ -136,14 +246,57 @@ final class MouseFinder {
|
|
|
136
246
|
}
|
|
137
247
|
|
|
138
248
|
func dismiss() {
|
|
249
|
+
removeEventMonitors()
|
|
139
250
|
animationTimer?.invalidate()
|
|
140
251
|
animationTimer = nil
|
|
141
252
|
dismissTimer?.invalidate()
|
|
142
253
|
dismissTimer = nil
|
|
143
|
-
|
|
254
|
+
sonarDelayTimer?.invalidate()
|
|
255
|
+
sonarDelayTimer = nil
|
|
256
|
+
for window in overlayWindows + sonarWindows {
|
|
144
257
|
window.orderOut(nil)
|
|
145
258
|
}
|
|
146
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
|
|
147
300
|
}
|
|
148
301
|
|
|
149
302
|
private func mouseScreen() -> NSScreen {
|
|
@@ -152,6 +305,164 @@ final class MouseFinder {
|
|
|
152
305
|
}
|
|
153
306
|
}
|
|
154
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
|
+
|
|
155
466
|
// MARK: - Sonar Ring View
|
|
156
467
|
|
|
157
468
|
private class SonarView: NSView {
|
|
@@ -172,7 +483,6 @@ private class SonarView: NSView {
|
|
|
172
483
|
|
|
173
484
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
174
485
|
|
|
175
|
-
// Draw rings from outermost to innermost
|
|
176
486
|
for i in 0..<ringCount {
|
|
177
487
|
let ringDelay = CGFloat(i) * 0.15
|
|
178
488
|
let denom = 1.0 - ringDelay * CGFloat(ringCount - 1) / CGFloat(ringCount)
|
|
@@ -180,13 +490,10 @@ private class SonarView: NSView {
|
|
|
180
490
|
|
|
181
491
|
guard ringProgress > 0 else { continue }
|
|
182
492
|
|
|
183
|
-
// Ease out cubic
|
|
184
493
|
let eased = 1.0 - pow(1.0 - ringProgress, 3)
|
|
185
|
-
|
|
186
494
|
let radius = maxRadius * eased
|
|
187
495
|
let alpha = (1.0 - eased) * 0.8
|
|
188
496
|
|
|
189
|
-
// Ring stroke
|
|
190
497
|
ctx.setStrokeColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: alpha).cgColor)
|
|
191
498
|
ctx.setLineWidth(2.5 - CGFloat(i) * 0.5)
|
|
192
499
|
ctx.addEllipse(in: CGRect(
|
|
@@ -198,7 +505,6 @@ private class SonarView: NSView {
|
|
|
198
505
|
ctx.strokePath()
|
|
199
506
|
}
|
|
200
507
|
|
|
201
|
-
// Center dot — stays visible
|
|
202
508
|
let dotRadius: CGFloat = 6
|
|
203
509
|
let dotAlpha = max(0.3, 1.0 - progress * 0.5)
|
|
204
510
|
ctx.setFillColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: dotAlpha).cgColor)
|
|
@@ -209,7 +515,6 @@ private class SonarView: NSView {
|
|
|
209
515
|
height: dotRadius * 2
|
|
210
516
|
))
|
|
211
517
|
|
|
212
|
-
// Outer glow on center dot
|
|
213
518
|
ctx.setFillColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: dotAlpha * 0.2).cgColor)
|
|
214
519
|
let glowRadius: CGFloat = 12
|
|
215
520
|
ctx.fillEllipse(in: CGRect(
|