@jian-ai/remote-control 0.8.0 → 0.8.2
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 +9 -1
- package/bin/computer-linux.mjs +537 -0
- package/bin/computer-macos.mjs +255 -59
- package/bin/computer-shared.mjs +61 -0
- package/bin/computer-windows.mjs +516 -0
- package/bin/computer.mjs +63 -0
- package/bin/jian-remote.mjs +44 -27
- package/linux/computer-helper.py +694 -0
- package/macos/build-helper.sh +46 -0
- package/macos/computer-helper +0 -0
- package/macos/computer-helper.swift +300 -131
- package/package.json +3 -1
- package/windows/computer-helper.ps1 +500 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build a universal (arm64 + x86_64) computer-helper for the npm package.
|
|
3
|
+
# Run on macOS with Xcode CLT. Used by Publish CI and local packaging.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
SRC="$ROOT/computer-helper.swift"
|
|
8
|
+
OUT="${1:-$ROOT/computer-helper}"
|
|
9
|
+
MIN_OS="${MACOSX_DEPLOYMENT_TARGET:-13.0}"
|
|
10
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/jian-computer-helper.XXXXXX")"
|
|
11
|
+
cleanup() { rm -rf "$TMP"; }
|
|
12
|
+
trap cleanup EXIT
|
|
13
|
+
|
|
14
|
+
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
15
|
+
echo "build-helper.sh must run on macOS (swiftc + lipo)." >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
if ! command -v swiftc >/dev/null 2>&1; then
|
|
19
|
+
echo "swiftc not found. Install Xcode Command Line Tools." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
if [[ ! -f "$SRC" ]]; then
|
|
23
|
+
echo "Missing source: $SRC" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
swiftc -O \
|
|
28
|
+
-target "arm64-apple-macosx${MIN_OS}" \
|
|
29
|
+
-framework AppKit -framework Carbon \
|
|
30
|
+
-o "$TMP/computer-helper-arm64" "$SRC"
|
|
31
|
+
|
|
32
|
+
swiftc -O \
|
|
33
|
+
-target "x86_64-apple-macosx${MIN_OS}" \
|
|
34
|
+
-framework AppKit -framework Carbon \
|
|
35
|
+
-o "$TMP/computer-helper-x86_64" "$SRC"
|
|
36
|
+
|
|
37
|
+
lipo -create -output "$OUT" "$TMP/computer-helper-arm64" "$TMP/computer-helper-x86_64"
|
|
38
|
+
chmod 755 "$OUT"
|
|
39
|
+
|
|
40
|
+
# Ad-hoc sign so Gatekeeper is quieter for LaunchAgent children; Developer ID overlays this in CI when secrets exist.
|
|
41
|
+
if command -v codesign >/dev/null 2>&1; then
|
|
42
|
+
codesign --force -s - --identifier com.jian.remote.computer-helper "$OUT" 2>/dev/null || true
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "Built universal helper: $OUT"
|
|
46
|
+
lipo -info "$OUT"
|
|
Binary file
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/*
|
|
2
2
|
Jian Remote Control — macOS computer-use helper
|
|
3
3
|
|
|
4
|
-
Long-lived AppKit process: overlay pointer + synthetic mouse/keyboard via CoreGraphics.
|
|
4
|
+
Long-lived AppKit process: Jian overlay pointer + synthetic mouse/keyboard via CoreGraphics.
|
|
5
5
|
Protocol: newline-delimited JSON on stdin → newline-delimited JSON on stdout (flush each).
|
|
6
6
|
Errors go to stderr in English only.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Pointer UX (Lucas / v0.8.2 polish):
|
|
9
|
+
- Geometric white arrow (anathema-white style) with a soft brand-blue glow underneath.
|
|
10
|
+
- Agent actions save the user’s cursor, hide it briefly, drive HID at the target, then
|
|
11
|
+
restore — so clicks do not leave the physical mouse stranded at the Jian position.
|
|
12
|
+
- Overlay tip animates (ease-out) between positions; system cursor is not the visible UX.
|
|
13
|
+
|
|
14
|
+
Published npm packages ship a prebuilt universal binary (Publish CI). From source on a Mac:
|
|
15
|
+
./build-helper.sh
|
|
16
|
+
# or: swiftc -O -framework AppKit -framework Carbon -o computer-helper computer-helper.swift
|
|
10
17
|
|
|
11
18
|
Run as a child of the Node worker; keep stdin/stdout pipes open for the session.
|
|
12
19
|
*/
|
|
@@ -72,16 +79,18 @@ private func intValue(_ object: [String: Any], _ key: String) -> Int? {
|
|
|
72
79
|
return nil
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
// MARK: - Colors (Jian
|
|
82
|
+
// MARK: - Colors (Jian + geometric white pointer)
|
|
76
83
|
|
|
77
|
-
private let jianCream = NSColor(srgbRed: 0xF5 / 255, green: 0xF5 / 255, blue: 0xF3 / 255, alpha: 1)
|
|
78
84
|
private let jianInk = NSColor(srgbRed: 0x17 / 255, green: 0x17 / 255, blue: 0x16 / 255, alpha: 1)
|
|
85
|
+
/// Soft brand blue under the pointer — visible, not neon.
|
|
86
|
+
private let jianGlow = NSColor(srgbRed: 0x3A / 255, green: 0x7A / 255, blue: 0xD6 / 255, alpha: 1)
|
|
87
|
+
private let pointerWhite = NSColor(srgbRed: 0xFA / 255, green: 0xFA / 255, blue: 0xF8 / 255, alpha: 1)
|
|
79
88
|
|
|
80
|
-
private let sessionAlpha: CGFloat = 0.
|
|
89
|
+
private let sessionAlpha: CGFloat = 0.94
|
|
81
90
|
private let idleAlpha: CGFloat = 0.18
|
|
82
|
-
private let pointerSize: CGFloat =
|
|
83
|
-
/// Tip of the
|
|
84
|
-
private let hotspot = CGPoint(x:
|
|
91
|
+
private let pointerSize: CGFloat = 36
|
|
92
|
+
/// Tip of the geometric arrow (hotspot) near the view's top-left.
|
|
93
|
+
private let hotspot = CGPoint(x: 2, y: 2)
|
|
85
94
|
|
|
86
95
|
// MARK: - Overlay view
|
|
87
96
|
|
|
@@ -97,11 +106,11 @@ private final class JianPointerView: NSView {
|
|
|
97
106
|
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
98
107
|
|
|
99
108
|
if rippleVisible {
|
|
100
|
-
let maxRadius = pointerSize * 0.
|
|
101
|
-
let radius =
|
|
109
|
+
let maxRadius = pointerSize * 0.9
|
|
110
|
+
let radius = 3 + maxRadius * rippleProgress
|
|
102
111
|
let alpha = max(0, 1 - rippleProgress)
|
|
103
112
|
ctx.saveGState()
|
|
104
|
-
ctx.setStrokeColor(
|
|
113
|
+
ctx.setStrokeColor(jianGlow.withAlphaComponent(0.45 * alpha).cgColor)
|
|
105
114
|
ctx.setLineWidth(1.5)
|
|
106
115
|
ctx.strokeEllipse(in: CGRect(
|
|
107
116
|
x: hotspot.x - radius,
|
|
@@ -112,57 +121,55 @@ private final class JianPointerView: NSView {
|
|
|
112
121
|
ctx.restoreGState()
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
let path = Self.
|
|
124
|
+
let path = Self.arrowPath(in: bounds)
|
|
125
|
+
|
|
126
|
+
// Soft blue glow under the white blade (brand cue without neon spam).
|
|
127
|
+
ctx.saveGState()
|
|
128
|
+
ctx.setShadow(
|
|
129
|
+
offset: CGSize(width: 0, height: 2),
|
|
130
|
+
blur: 10,
|
|
131
|
+
color: jianGlow.withAlphaComponent(0.55).cgColor
|
|
132
|
+
)
|
|
133
|
+
jianGlow.withAlphaComponent(0.35).setFill()
|
|
134
|
+
path.fill()
|
|
135
|
+
ctx.restoreGState()
|
|
136
|
+
|
|
116
137
|
ctx.saveGState()
|
|
117
|
-
ctx.setShadow(
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
ctx.setShadow(
|
|
139
|
+
offset: CGSize(width: 0.5, height: 1.2),
|
|
140
|
+
blur: 2.5,
|
|
141
|
+
color: NSColor.black.withAlphaComponent(0.22).cgColor
|
|
142
|
+
)
|
|
143
|
+
let fillAlpha = 1 - fillPulse * 0.28
|
|
144
|
+
pointerWhite.withAlphaComponent(fillAlpha).setFill()
|
|
120
145
|
path.fill()
|
|
121
146
|
ctx.restoreGState()
|
|
122
147
|
|
|
123
|
-
jianInk.setStroke()
|
|
124
|
-
path.lineWidth =
|
|
125
|
-
path.lineJoinStyle = .
|
|
126
|
-
path.lineCapStyle = .
|
|
148
|
+
jianInk.withAlphaComponent(0.85).setStroke()
|
|
149
|
+
path.lineWidth = 1.15
|
|
150
|
+
path.lineJoinStyle = .miter
|
|
151
|
+
path.lineCapStyle = .butt
|
|
152
|
+
path.miterLimit = 8
|
|
127
153
|
path.stroke()
|
|
128
154
|
}
|
|
129
155
|
|
|
130
|
-
///
|
|
131
|
-
private static func
|
|
156
|
+
/// Sharp geometric pointer (anathema-white style): tip → shaft → barb.
|
|
157
|
+
private static func arrowPath(in bounds: NSRect) -> NSBezierPath {
|
|
132
158
|
let s = min(bounds.width, bounds.height)
|
|
133
159
|
let path = NSBezierPath()
|
|
134
|
-
//
|
|
135
|
-
path.move(to: CGPoint(x: s * 0.
|
|
136
|
-
path.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
path.
|
|
142
|
-
to: CGPoint(x: s * 0.88, y: s * 0.55),
|
|
143
|
-
controlPoint1: CGPoint(x: s * 0.88, y: s * 0.28),
|
|
144
|
-
controlPoint2: CGPoint(x: s * 0.94, y: s * 0.42)
|
|
145
|
-
)
|
|
146
|
-
path.curve(
|
|
147
|
-
to: CGPoint(x: s * 0.42, y: s * 0.88),
|
|
148
|
-
controlPoint1: CGPoint(x: s * 0.80, y: s * 0.72),
|
|
149
|
-
controlPoint2: CGPoint(x: s * 0.58, y: s * 0.90)
|
|
150
|
-
)
|
|
151
|
-
path.curve(
|
|
152
|
-
to: CGPoint(x: s * 0.18, y: s * 0.48),
|
|
153
|
-
controlPoint1: CGPoint(x: s * 0.26, y: s * 0.86),
|
|
154
|
-
controlPoint2: CGPoint(x: s * 0.12, y: s * 0.68)
|
|
155
|
-
)
|
|
156
|
-
path.curve(
|
|
157
|
-
to: CGPoint(x: s * 0.12, y: s * 0.10),
|
|
158
|
-
controlPoint1: CGPoint(x: s * 0.22, y: s * 0.32),
|
|
159
|
-
controlPoint2: CGPoint(x: s * 0.08, y: s * 0.20)
|
|
160
|
-
)
|
|
160
|
+
// Coordinates in view space (flipped Y): tip near top-left hotspot.
|
|
161
|
+
path.move(to: CGPoint(x: s * 0.06, y: s * 0.06))
|
|
162
|
+
path.line(to: CGPoint(x: s * 0.06, y: s * 0.72))
|
|
163
|
+
path.line(to: CGPoint(x: s * 0.28, y: s * 0.55))
|
|
164
|
+
path.line(to: CGPoint(x: s * 0.42, y: s * 0.88))
|
|
165
|
+
path.line(to: CGPoint(x: s * 0.55, y: s * 0.82))
|
|
166
|
+
path.line(to: CGPoint(x: s * 0.36, y: s * 0.48))
|
|
167
|
+
path.line(to: CGPoint(x: s * 0.62, y: s * 0.48))
|
|
161
168
|
path.close()
|
|
162
169
|
return path
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
func flashClick(duration: TimeInterval = 0.
|
|
172
|
+
func flashClick(duration: TimeInterval = 0.16) {
|
|
166
173
|
rippleTimer?.invalidate()
|
|
167
174
|
rippleVisible = true
|
|
168
175
|
rippleProgress = 0
|
|
@@ -206,6 +213,7 @@ private final class OverlayController {
|
|
|
206
213
|
private var pointerView: JianPointerView?
|
|
207
214
|
/// Current tip/hotspot in global top-left Quartz coordinates.
|
|
208
215
|
private var tipPoint = CGPoint.zero
|
|
216
|
+
private var hasTip = false
|
|
209
217
|
|
|
210
218
|
private init() {}
|
|
211
219
|
|
|
@@ -240,6 +248,10 @@ private final class OverlayController {
|
|
|
240
248
|
ensureWindow()
|
|
241
249
|
guard let window else { return }
|
|
242
250
|
window.alphaValue = sessionAlpha
|
|
251
|
+
if !hasTip {
|
|
252
|
+
tipPoint = quartzMouseLocation()
|
|
253
|
+
hasTip = true
|
|
254
|
+
}
|
|
243
255
|
positionWindow()
|
|
244
256
|
window.orderFrontRegardless()
|
|
245
257
|
}
|
|
@@ -256,10 +268,12 @@ private final class OverlayController {
|
|
|
256
268
|
guard let window else { return }
|
|
257
269
|
window.orderOut(nil)
|
|
258
270
|
window.alphaValue = 0
|
|
271
|
+
hasTip = false
|
|
259
272
|
}
|
|
260
273
|
|
|
261
274
|
func moveTip(to globalTopLeft: CGPoint) {
|
|
262
275
|
tipPoint = globalTopLeft
|
|
276
|
+
hasTip = true
|
|
263
277
|
ensureWindow()
|
|
264
278
|
positionWindow()
|
|
265
279
|
}
|
|
@@ -276,7 +290,6 @@ private final class OverlayController {
|
|
|
276
290
|
// Quartz global: origin top-left of primary, Y down. Cocoa: origin bottom-left, Y up.
|
|
277
291
|
let mainHeight = NSScreen.screens.first?.frame.height ?? NSScreen.main?.frame.height ?? 0
|
|
278
292
|
let tipCocoaY = mainHeight - tipPoint.y
|
|
279
|
-
// Window origin is bottom-left; hotspot is hotspot.y down from the top of the view.
|
|
280
293
|
let origin = NSPoint(
|
|
281
294
|
x: tipPoint.x - hotspot.x,
|
|
282
295
|
y: tipCocoaY - (pointerSize - hotspot.y)
|
|
@@ -285,105 +298,175 @@ private final class OverlayController {
|
|
|
285
298
|
}
|
|
286
299
|
}
|
|
287
300
|
|
|
288
|
-
// MARK: -
|
|
301
|
+
// MARK: - Cursor location / preserve
|
|
289
302
|
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
/// Quartz global top-left coordinates for the live system cursor.
|
|
304
|
+
private func quartzMouseLocation() -> CGPoint {
|
|
305
|
+
let cocoa = NSEvent.mouseLocation
|
|
306
|
+
let mainHeight = NSScreen.screens.first?.frame.height ?? NSScreen.main?.frame.height ?? 0
|
|
307
|
+
return CGPoint(x: cocoa.x, y: mainHeight - cocoa.y)
|
|
292
308
|
}
|
|
293
309
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
private func postMouseMove(to point: CGPoint) {
|
|
297
|
-
OverlayController.shared.moveTip(to: point)
|
|
310
|
+
/// Warp HID cursor without moving the Jian overlay tip.
|
|
311
|
+
private func warpSystemCursor(to point: CGPoint) {
|
|
298
312
|
guard let event = CGEvent(
|
|
299
313
|
mouseEventSource: nil,
|
|
300
314
|
mouseType: .mouseMoved,
|
|
301
315
|
mouseCursorPosition: point,
|
|
302
316
|
mouseButton: .left
|
|
303
317
|
) else {
|
|
304
|
-
logError("could not create mouseMoved event")
|
|
318
|
+
logError("could not create mouseMoved event for warp")
|
|
305
319
|
return
|
|
306
320
|
}
|
|
307
321
|
event.post(tap: .cghidEventTap)
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
/**
|
|
325
|
+
Save → hide → act → restore → show.
|
|
326
|
+
|
|
327
|
+
HID clicks must be posted at the target (macOS delivers them at the event location),
|
|
328
|
+
but we restore immediately so Lucas’s physical mouse is not left at the Jian point.
|
|
329
|
+
*/
|
|
330
|
+
private func withPreservedUserCursor(_ body: () -> Void) {
|
|
331
|
+
let saved = quartzMouseLocation()
|
|
332
|
+
CGDisplayHideCursor(CGMainDisplayID())
|
|
333
|
+
body()
|
|
334
|
+
warpSystemCursor(to: saved)
|
|
335
|
+
// Tiny yield so the restore event lands before unhide.
|
|
336
|
+
Thread.sleep(forTimeInterval: 0.008)
|
|
337
|
+
CGDisplayShowCursor(CGMainDisplayID())
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// MARK: - Smooth overlay motion
|
|
341
|
+
|
|
342
|
+
private func easeOutCubic(_ t: Double) -> Double {
|
|
343
|
+
let x = min(1, max(0, t))
|
|
344
|
+
return 1 - pow(1 - x, 3)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private func animateOverlay(to target: CGPoint) {
|
|
348
|
+
OverlayController.shared.ensureSession()
|
|
349
|
+
let start = OverlayController.shared.currentTip()
|
|
350
|
+
let distance = hypot(target.x - start.x, target.y - start.y)
|
|
351
|
+
if distance < 1.5 {
|
|
352
|
+
OverlayController.shared.moveTip(to: target)
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
// ~0.25 ms/px, clamped — snappy for agent loops.
|
|
356
|
+
let duration = min(0.11, max(0.028, distance * 0.00025))
|
|
357
|
+
let steps = max(1, Int(ceil(duration * 60)))
|
|
358
|
+
let stepSleep = duration / Double(steps)
|
|
359
|
+
for i in 1...steps {
|
|
360
|
+
let t = easeOutCubic(Double(i) / Double(steps))
|
|
361
|
+
let p = CGPoint(
|
|
362
|
+
x: start.x + (target.x - start.x) * t,
|
|
363
|
+
y: start.y + (target.y - start.y) * t
|
|
364
|
+
)
|
|
365
|
+
OverlayController.shared.moveTip(to: p)
|
|
366
|
+
if i < steps {
|
|
367
|
+
RunLoop.current.run(until: Date(timeIntervalSinceNow: stepSleep))
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
OverlayController.shared.moveTip(to: target)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// MARK: - Mouse / keyboard
|
|
374
|
+
|
|
375
|
+
private func postOverlayMove(to point: CGPoint) {
|
|
376
|
+
animateOverlay(to: point)
|
|
377
|
+
}
|
|
378
|
+
|
|
310
379
|
private func postMouseClick(button: CGMouseButton, count: Int64) {
|
|
311
380
|
OverlayController.shared.flashClick()
|
|
312
381
|
let point = OverlayController.shared.currentTip()
|
|
313
382
|
let downType: CGEventType = button == .right ? .rightMouseDown : .leftMouseDown
|
|
314
383
|
let upType: CGEventType = button == .right ? .rightMouseUp : .leftMouseUp
|
|
315
384
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
385
|
+
withPreservedUserCursor {
|
|
386
|
+
warpSystemCursor(to: point)
|
|
387
|
+
for clickIndex in 1...max(1, count) {
|
|
388
|
+
if let down = CGEvent(
|
|
389
|
+
mouseEventSource: nil,
|
|
390
|
+
mouseType: downType,
|
|
391
|
+
mouseCursorPosition: point,
|
|
392
|
+
mouseButton: button
|
|
393
|
+
) {
|
|
394
|
+
down.setIntegerValueField(.mouseEventClickState, value: clickIndex)
|
|
395
|
+
down.post(tap: .cghidEventTap)
|
|
396
|
+
}
|
|
397
|
+
if let up = CGEvent(
|
|
398
|
+
mouseEventSource: nil,
|
|
399
|
+
mouseType: upType,
|
|
400
|
+
mouseCursorPosition: point,
|
|
401
|
+
mouseButton: button
|
|
402
|
+
) {
|
|
403
|
+
up.setIntegerValueField(.mouseEventClickState, value: clickIndex)
|
|
404
|
+
up.post(tap: .cghidEventTap)
|
|
405
|
+
}
|
|
406
|
+
if clickIndex < count {
|
|
407
|
+
Thread.sleep(forTimeInterval: 0.04)
|
|
408
|
+
}
|
|
337
409
|
}
|
|
338
410
|
}
|
|
339
411
|
}
|
|
340
412
|
|
|
413
|
+
private func postClickAt(point: CGPoint, button: CGMouseButton, count: Int64) {
|
|
414
|
+
animateOverlay(to: point)
|
|
415
|
+
postMouseClick(button: button, count: count)
|
|
416
|
+
}
|
|
417
|
+
|
|
341
418
|
private func postDrag(from: CGPoint, to: CGPoint) {
|
|
342
|
-
|
|
419
|
+
OverlayController.shared.ensureSession()
|
|
420
|
+
animateOverlay(to: from)
|
|
343
421
|
OverlayController.shared.flashClick()
|
|
344
|
-
Thread.sleep(forTimeInterval: 0.02)
|
|
345
422
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
mouseCursorPosition: from,
|
|
350
|
-
mouseButton: .left
|
|
351
|
-
) {
|
|
352
|
-
down.post(tap: .cghidEventTap)
|
|
353
|
-
}
|
|
423
|
+
withPreservedUserCursor {
|
|
424
|
+
warpSystemCursor(to: from)
|
|
425
|
+
Thread.sleep(forTimeInterval: 0.012)
|
|
354
426
|
|
|
355
|
-
|
|
356
|
-
for i in 1...steps {
|
|
357
|
-
let t = CGFloat(i) / CGFloat(steps)
|
|
358
|
-
let p = CGPoint(
|
|
359
|
-
x: from.x + (to.x - from.x) * t,
|
|
360
|
-
y: from.y + (to.y - from.y) * t
|
|
361
|
-
)
|
|
362
|
-
OverlayController.shared.moveTip(to: p)
|
|
363
|
-
if let drag = CGEvent(
|
|
427
|
+
if let down = CGEvent(
|
|
364
428
|
mouseEventSource: nil,
|
|
365
|
-
mouseType: .
|
|
366
|
-
mouseCursorPosition:
|
|
429
|
+
mouseType: .leftMouseDown,
|
|
430
|
+
mouseCursorPosition: from,
|
|
367
431
|
mouseButton: .left
|
|
368
432
|
) {
|
|
369
|
-
|
|
433
|
+
down.post(tap: .cghidEventTap)
|
|
370
434
|
}
|
|
371
|
-
Thread.sleep(forTimeInterval: 0.008)
|
|
372
|
-
}
|
|
373
435
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
436
|
+
let distance = hypot(to.x - from.x, to.y - from.y)
|
|
437
|
+
let steps = max(6, min(14, Int(distance / 40) + 6))
|
|
438
|
+
for i in 1...steps {
|
|
439
|
+
let t = easeOutCubic(Double(i) / Double(steps))
|
|
440
|
+
let p = CGPoint(
|
|
441
|
+
x: from.x + (to.x - from.x) * t,
|
|
442
|
+
y: from.y + (to.y - from.y) * t
|
|
443
|
+
)
|
|
444
|
+
OverlayController.shared.moveTip(to: p)
|
|
445
|
+
if let drag = CGEvent(
|
|
446
|
+
mouseEventSource: nil,
|
|
447
|
+
mouseType: .leftMouseDragged,
|
|
448
|
+
mouseCursorPosition: p,
|
|
449
|
+
mouseButton: .left
|
|
450
|
+
) {
|
|
451
|
+
drag.post(tap: .cghidEventTap)
|
|
452
|
+
}
|
|
453
|
+
Thread.sleep(forTimeInterval: 0.006)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if let up = CGEvent(
|
|
457
|
+
mouseEventSource: nil,
|
|
458
|
+
mouseType: .leftMouseUp,
|
|
459
|
+
mouseCursorPosition: to,
|
|
460
|
+
mouseButton: .left
|
|
461
|
+
) {
|
|
462
|
+
up.post(tap: .cghidEventTap)
|
|
463
|
+
}
|
|
381
464
|
}
|
|
382
465
|
OverlayController.shared.moveTip(to: to)
|
|
383
466
|
}
|
|
384
467
|
|
|
385
468
|
private func postScroll(at point: CGPoint, direction: String, amount: Int) {
|
|
386
|
-
|
|
469
|
+
animateOverlay(to: point)
|
|
387
470
|
let n = max(1, amount)
|
|
388
471
|
var dx = 0
|
|
389
472
|
var dy = 0
|
|
@@ -396,18 +479,21 @@ private func postScroll(at point: CGPoint, direction: String, amount: Int) {
|
|
|
396
479
|
logError("unknown scroll direction: \(direction)")
|
|
397
480
|
return
|
|
398
481
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
482
|
+
withPreservedUserCursor {
|
|
483
|
+
warpSystemCursor(to: point)
|
|
484
|
+
for _ in 0..<n {
|
|
485
|
+
if let event = CGEvent(
|
|
486
|
+
scrollWheelEvent2Source: nil,
|
|
487
|
+
units: .line,
|
|
488
|
+
wheelCount: 2,
|
|
489
|
+
wheel1: Int32(dy),
|
|
490
|
+
wheel2: Int32(dx),
|
|
491
|
+
wheel3: 0
|
|
492
|
+
) {
|
|
493
|
+
event.post(tap: .cghidEventTap)
|
|
494
|
+
}
|
|
495
|
+
Thread.sleep(forTimeInterval: 0.012)
|
|
409
496
|
}
|
|
410
|
-
Thread.sleep(forTimeInterval: 0.02)
|
|
411
497
|
}
|
|
412
498
|
}
|
|
413
499
|
|
|
@@ -479,7 +565,6 @@ private func postUnicodeChar(_ scalar: UniChar) {
|
|
|
479
565
|
}
|
|
480
566
|
|
|
481
567
|
private func typeText(_ text: String) {
|
|
482
|
-
// Iterate UTF-16 code units; map Return/Tab to virtual keys when present as characters.
|
|
483
568
|
let utf16 = Array(text.utf16)
|
|
484
569
|
var i = 0
|
|
485
570
|
while i < utf16.count {
|
|
@@ -495,7 +580,7 @@ private func typeText(_ text: String) {
|
|
|
495
580
|
postUnicodeChar(unit)
|
|
496
581
|
}
|
|
497
582
|
i += 1
|
|
498
|
-
Thread.sleep(forTimeInterval: 0.
|
|
583
|
+
Thread.sleep(forTimeInterval: 0.002)
|
|
499
584
|
}
|
|
500
585
|
}
|
|
501
586
|
|
|
@@ -546,7 +631,6 @@ private func parseAndPostKey(_ keySpec: String) -> Bool {
|
|
|
546
631
|
}
|
|
547
632
|
}
|
|
548
633
|
|
|
549
|
-
// Function keys F1–F12
|
|
550
634
|
if token.hasPrefix("f"), let n = Int(token.dropFirst()), (1...12).contains(n) {
|
|
551
635
|
let base = kVK_F1
|
|
552
636
|
postKeyCode(CGKeyCode(base + (n - 1)), flags: flags)
|
|
@@ -566,7 +650,6 @@ private func geometryResponse() -> [String: Any] {
|
|
|
566
650
|
}
|
|
567
651
|
let frame = screen.frame
|
|
568
652
|
let scale = screen.backingScaleFactor
|
|
569
|
-
// Primary display: Cocoa origin is bottom-left of main screen; Quartz global top-left origin is (0,0).
|
|
570
653
|
return [
|
|
571
654
|
"ok": true,
|
|
572
655
|
"originX": 0,
|
|
@@ -591,6 +674,33 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
591
674
|
case "ping":
|
|
592
675
|
writeOk()
|
|
593
676
|
|
|
677
|
+
case "sessionStatus":
|
|
678
|
+
var locked = false
|
|
679
|
+
if let raw = CGSessionCopyCurrentDictionary() {
|
|
680
|
+
let dict = raw as NSDictionary
|
|
681
|
+
if let number = dict["CGSSessionScreenIsLocked"] as? NSNumber {
|
|
682
|
+
locked = number.boolValue
|
|
683
|
+
} else if let flag = dict["CGSSessionScreenIsLocked"] as? Bool {
|
|
684
|
+
locked = flag
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// CGDisplayIsAsleep returns boolean_t (Int32); Swift will not coerce it in `if`.
|
|
688
|
+
let displayAsleep = CGDisplayIsAsleep(CGMainDisplayID()) != 0
|
|
689
|
+
let state: String
|
|
690
|
+
if locked {
|
|
691
|
+
state = "locked"
|
|
692
|
+
} else if displayAsleep {
|
|
693
|
+
state = "display_asleep"
|
|
694
|
+
} else {
|
|
695
|
+
state = "unlocked"
|
|
696
|
+
}
|
|
697
|
+
writeResponse([
|
|
698
|
+
"ok": true,
|
|
699
|
+
"state": state,
|
|
700
|
+
"locked": locked,
|
|
701
|
+
"displayAsleep": displayAsleep,
|
|
702
|
+
])
|
|
703
|
+
|
|
594
704
|
case "geometry":
|
|
595
705
|
writeResponse(geometryResponse())
|
|
596
706
|
|
|
@@ -611,7 +721,8 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
611
721
|
writeError("move requires pointX and pointY")
|
|
612
722
|
return
|
|
613
723
|
}
|
|
614
|
-
|
|
724
|
+
// Overlay-only: do not warp the user’s system cursor.
|
|
725
|
+
postOverlayMove(to: CGPoint(x: x, y: y))
|
|
615
726
|
writeOk()
|
|
616
727
|
|
|
617
728
|
case "click":
|
|
@@ -632,6 +743,28 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
632
743
|
postMouseClick(button: button, count: count)
|
|
633
744
|
writeOk()
|
|
634
745
|
|
|
746
|
+
case "clickAt":
|
|
747
|
+
guard let x = doubleValue(object, "pointX"), let y = doubleValue(object, "pointY") else {
|
|
748
|
+
writeError("clickAt requires pointX and pointY")
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
let buttonName = (object["button"] as? String ?? "left").lowercased()
|
|
752
|
+
let count = Int64(intValue(object, "count") ?? 1)
|
|
753
|
+
let button: CGMouseButton
|
|
754
|
+
switch buttonName {
|
|
755
|
+
case "left": button = .left
|
|
756
|
+
case "right": button = .right
|
|
757
|
+
default:
|
|
758
|
+
writeError("button must be left or right")
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
if count != 1 && count != 2 {
|
|
762
|
+
writeError("count must be 1 or 2")
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
postClickAt(point: CGPoint(x: x, y: y), button: button, count: count)
|
|
766
|
+
writeOk()
|
|
767
|
+
|
|
635
768
|
case "drag":
|
|
636
769
|
guard let fromX = doubleValue(object, "fromX"),
|
|
637
770
|
let fromY = doubleValue(object, "fromY"),
|
|
@@ -641,7 +774,7 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
641
774
|
writeError("drag requires fromX, fromY, toX, toY")
|
|
642
775
|
return
|
|
643
776
|
}
|
|
644
|
-
postDrag(from:
|
|
777
|
+
postDrag(from: CGPoint(x: fromX, y: fromY), to: CGPoint(x: toX, y: toY))
|
|
645
778
|
writeOk()
|
|
646
779
|
|
|
647
780
|
case "type":
|
|
@@ -677,7 +810,7 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
677
810
|
writeError("direction must be up, down, left, or right")
|
|
678
811
|
return
|
|
679
812
|
}
|
|
680
|
-
postScroll(at:
|
|
813
|
+
postScroll(at: CGPoint(x: x, y: y), direction: dir, amount: amount)
|
|
681
814
|
writeOk()
|
|
682
815
|
|
|
683
816
|
default:
|
|
@@ -697,7 +830,6 @@ private final class StdinReader {
|
|
|
697
830
|
while true {
|
|
698
831
|
let chunk = handle.availableData
|
|
699
832
|
if chunk.isEmpty {
|
|
700
|
-
// EOF — exit the process cleanly.
|
|
701
833
|
DispatchQueue.main.async {
|
|
702
834
|
NSApp.terminate(nil)
|
|
703
835
|
}
|
|
@@ -725,8 +857,45 @@ private final class StdinReader {
|
|
|
725
857
|
}
|
|
726
858
|
}
|
|
727
859
|
|
|
860
|
+
// MARK: - Session status (one-shot, no AppKit run loop)
|
|
861
|
+
|
|
862
|
+
/// Reports lock / display-asleep without starting the overlay. Used by the Node worker poll
|
|
863
|
+
/// so a locked Mac stays "online" with an explicit uiState instead of looking dead.
|
|
864
|
+
private func printSessionStatusAndExit() -> Never {
|
|
865
|
+
var locked = false
|
|
866
|
+
if let raw = CGSessionCopyCurrentDictionary() {
|
|
867
|
+
let dict = raw as NSDictionary
|
|
868
|
+
if let number = dict["CGSSessionScreenIsLocked"] as? NSNumber {
|
|
869
|
+
locked = number.boolValue
|
|
870
|
+
} else if let flag = dict["CGSSessionScreenIsLocked"] as? Bool {
|
|
871
|
+
locked = flag
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// CGDisplayIsAsleep returns boolean_t (Int32); Swift will not coerce it in `if`.
|
|
875
|
+
let displayAsleep = CGDisplayIsAsleep(CGMainDisplayID()) != 0
|
|
876
|
+
let state: String
|
|
877
|
+
if locked {
|
|
878
|
+
state = "locked"
|
|
879
|
+
} else if displayAsleep {
|
|
880
|
+
state = "display_asleep"
|
|
881
|
+
} else {
|
|
882
|
+
state = "unlocked"
|
|
883
|
+
}
|
|
884
|
+
writeResponse([
|
|
885
|
+
"ok": true,
|
|
886
|
+
"state": state,
|
|
887
|
+
"locked": locked,
|
|
888
|
+
"displayAsleep": displayAsleep,
|
|
889
|
+
])
|
|
890
|
+
exit(0)
|
|
891
|
+
}
|
|
892
|
+
|
|
728
893
|
private let stdinReader = StdinReader()
|
|
729
894
|
|
|
895
|
+
if CommandLine.arguments.contains("--session-status") {
|
|
896
|
+
printSessionStatusAndExit()
|
|
897
|
+
}
|
|
898
|
+
|
|
730
899
|
autoreleasepool {
|
|
731
900
|
let app = NSApplication.shared
|
|
732
901
|
app.setActivationPolicy(.accessory)
|