@jian-ai/remote-control 0.8.1 → 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 -3
- package/bin/computer-linux.mjs +537 -0
- package/bin/computer-macos.mjs +171 -41
- 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/computer-helper +0 -0
- package/macos/computer-helper.swift +297 -129
- package/package.json +3 -1
- package/windows/computer-helper.ps1 +500 -0
|
@@ -1,10 +1,16 @@
|
|
|
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
|
+
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
|
+
|
|
8
14
|
Published npm packages ship a prebuilt universal binary (Publish CI). From source on a Mac:
|
|
9
15
|
./build-helper.sh
|
|
10
16
|
# or: swiftc -O -framework AppKit -framework Carbon -o computer-helper computer-helper.swift
|
|
@@ -73,16 +79,18 @@ private func intValue(_ object: [String: Any], _ key: String) -> Int? {
|
|
|
73
79
|
return nil
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
// MARK: - Colors (Jian
|
|
82
|
+
// MARK: - Colors (Jian + geometric white pointer)
|
|
77
83
|
|
|
78
|
-
private let jianCream = NSColor(srgbRed: 0xF5 / 255, green: 0xF5 / 255, blue: 0xF3 / 255, alpha: 1)
|
|
79
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)
|
|
80
88
|
|
|
81
|
-
private let sessionAlpha: CGFloat = 0.
|
|
89
|
+
private let sessionAlpha: CGFloat = 0.94
|
|
82
90
|
private let idleAlpha: CGFloat = 0.18
|
|
83
|
-
private let pointerSize: CGFloat =
|
|
84
|
-
/// Tip of the
|
|
85
|
-
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)
|
|
86
94
|
|
|
87
95
|
// MARK: - Overlay view
|
|
88
96
|
|
|
@@ -98,11 +106,11 @@ private final class JianPointerView: NSView {
|
|
|
98
106
|
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
99
107
|
|
|
100
108
|
if rippleVisible {
|
|
101
|
-
let maxRadius = pointerSize * 0.
|
|
102
|
-
let radius =
|
|
109
|
+
let maxRadius = pointerSize * 0.9
|
|
110
|
+
let radius = 3 + maxRadius * rippleProgress
|
|
103
111
|
let alpha = max(0, 1 - rippleProgress)
|
|
104
112
|
ctx.saveGState()
|
|
105
|
-
ctx.setStrokeColor(
|
|
113
|
+
ctx.setStrokeColor(jianGlow.withAlphaComponent(0.45 * alpha).cgColor)
|
|
106
114
|
ctx.setLineWidth(1.5)
|
|
107
115
|
ctx.strokeEllipse(in: CGRect(
|
|
108
116
|
x: hotspot.x - radius,
|
|
@@ -113,57 +121,55 @@ private final class JianPointerView: NSView {
|
|
|
113
121
|
ctx.restoreGState()
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
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
|
+
|
|
117
137
|
ctx.saveGState()
|
|
118
|
-
ctx.setShadow(
|
|
119
|
-
|
|
120
|
-
|
|
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()
|
|
121
145
|
path.fill()
|
|
122
146
|
ctx.restoreGState()
|
|
123
147
|
|
|
124
|
-
jianInk.setStroke()
|
|
125
|
-
path.lineWidth =
|
|
126
|
-
path.lineJoinStyle = .
|
|
127
|
-
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
|
|
128
153
|
path.stroke()
|
|
129
154
|
}
|
|
130
155
|
|
|
131
|
-
///
|
|
132
|
-
private static func
|
|
156
|
+
/// Sharp geometric pointer (anathema-white style): tip → shaft → barb.
|
|
157
|
+
private static func arrowPath(in bounds: NSRect) -> NSBezierPath {
|
|
133
158
|
let s = min(bounds.width, bounds.height)
|
|
134
159
|
let path = NSBezierPath()
|
|
135
|
-
//
|
|
136
|
-
path.move(to: CGPoint(x: s * 0.
|
|
137
|
-
path.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
path.
|
|
143
|
-
to: CGPoint(x: s * 0.88, y: s * 0.55),
|
|
144
|
-
controlPoint1: CGPoint(x: s * 0.88, y: s * 0.28),
|
|
145
|
-
controlPoint2: CGPoint(x: s * 0.94, y: s * 0.42)
|
|
146
|
-
)
|
|
147
|
-
path.curve(
|
|
148
|
-
to: CGPoint(x: s * 0.42, y: s * 0.88),
|
|
149
|
-
controlPoint1: CGPoint(x: s * 0.80, y: s * 0.72),
|
|
150
|
-
controlPoint2: CGPoint(x: s * 0.58, y: s * 0.90)
|
|
151
|
-
)
|
|
152
|
-
path.curve(
|
|
153
|
-
to: CGPoint(x: s * 0.18, y: s * 0.48),
|
|
154
|
-
controlPoint1: CGPoint(x: s * 0.26, y: s * 0.86),
|
|
155
|
-
controlPoint2: CGPoint(x: s * 0.12, y: s * 0.68)
|
|
156
|
-
)
|
|
157
|
-
path.curve(
|
|
158
|
-
to: CGPoint(x: s * 0.12, y: s * 0.10),
|
|
159
|
-
controlPoint1: CGPoint(x: s * 0.22, y: s * 0.32),
|
|
160
|
-
controlPoint2: CGPoint(x: s * 0.08, y: s * 0.20)
|
|
161
|
-
)
|
|
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))
|
|
162
168
|
path.close()
|
|
163
169
|
return path
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
func flashClick(duration: TimeInterval = 0.
|
|
172
|
+
func flashClick(duration: TimeInterval = 0.16) {
|
|
167
173
|
rippleTimer?.invalidate()
|
|
168
174
|
rippleVisible = true
|
|
169
175
|
rippleProgress = 0
|
|
@@ -207,6 +213,7 @@ private final class OverlayController {
|
|
|
207
213
|
private var pointerView: JianPointerView?
|
|
208
214
|
/// Current tip/hotspot in global top-left Quartz coordinates.
|
|
209
215
|
private var tipPoint = CGPoint.zero
|
|
216
|
+
private var hasTip = false
|
|
210
217
|
|
|
211
218
|
private init() {}
|
|
212
219
|
|
|
@@ -241,6 +248,10 @@ private final class OverlayController {
|
|
|
241
248
|
ensureWindow()
|
|
242
249
|
guard let window else { return }
|
|
243
250
|
window.alphaValue = sessionAlpha
|
|
251
|
+
if !hasTip {
|
|
252
|
+
tipPoint = quartzMouseLocation()
|
|
253
|
+
hasTip = true
|
|
254
|
+
}
|
|
244
255
|
positionWindow()
|
|
245
256
|
window.orderFrontRegardless()
|
|
246
257
|
}
|
|
@@ -257,10 +268,12 @@ private final class OverlayController {
|
|
|
257
268
|
guard let window else { return }
|
|
258
269
|
window.orderOut(nil)
|
|
259
270
|
window.alphaValue = 0
|
|
271
|
+
hasTip = false
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
func moveTip(to globalTopLeft: CGPoint) {
|
|
263
275
|
tipPoint = globalTopLeft
|
|
276
|
+
hasTip = true
|
|
264
277
|
ensureWindow()
|
|
265
278
|
positionWindow()
|
|
266
279
|
}
|
|
@@ -277,7 +290,6 @@ private final class OverlayController {
|
|
|
277
290
|
// Quartz global: origin top-left of primary, Y down. Cocoa: origin bottom-left, Y up.
|
|
278
291
|
let mainHeight = NSScreen.screens.first?.frame.height ?? NSScreen.main?.frame.height ?? 0
|
|
279
292
|
let tipCocoaY = mainHeight - tipPoint.y
|
|
280
|
-
// Window origin is bottom-left; hotspot is hotspot.y down from the top of the view.
|
|
281
293
|
let origin = NSPoint(
|
|
282
294
|
x: tipPoint.x - hotspot.x,
|
|
283
295
|
y: tipCocoaY - (pointerSize - hotspot.y)
|
|
@@ -286,105 +298,175 @@ private final class OverlayController {
|
|
|
286
298
|
}
|
|
287
299
|
}
|
|
288
300
|
|
|
289
|
-
// MARK: -
|
|
301
|
+
// MARK: - Cursor location / preserve
|
|
290
302
|
|
|
291
|
-
|
|
292
|
-
|
|
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)
|
|
293
308
|
}
|
|
294
309
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
private func postMouseMove(to point: CGPoint) {
|
|
298
|
-
OverlayController.shared.moveTip(to: point)
|
|
310
|
+
/// Warp HID cursor without moving the Jian overlay tip.
|
|
311
|
+
private func warpSystemCursor(to point: CGPoint) {
|
|
299
312
|
guard let event = CGEvent(
|
|
300
313
|
mouseEventSource: nil,
|
|
301
314
|
mouseType: .mouseMoved,
|
|
302
315
|
mouseCursorPosition: point,
|
|
303
316
|
mouseButton: .left
|
|
304
317
|
) else {
|
|
305
|
-
logError("could not create mouseMoved event")
|
|
318
|
+
logError("could not create mouseMoved event for warp")
|
|
306
319
|
return
|
|
307
320
|
}
|
|
308
321
|
event.post(tap: .cghidEventTap)
|
|
309
322
|
}
|
|
310
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
|
+
|
|
311
379
|
private func postMouseClick(button: CGMouseButton, count: Int64) {
|
|
312
380
|
OverlayController.shared.flashClick()
|
|
313
381
|
let point = OverlayController.shared.currentTip()
|
|
314
382
|
let downType: CGEventType = button == .right ? .rightMouseDown : .leftMouseDown
|
|
315
383
|
let upType: CGEventType = button == .right ? .rightMouseUp : .leftMouseUp
|
|
316
384
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}
|
|
338
409
|
}
|
|
339
410
|
}
|
|
340
411
|
}
|
|
341
412
|
|
|
413
|
+
private func postClickAt(point: CGPoint, button: CGMouseButton, count: Int64) {
|
|
414
|
+
animateOverlay(to: point)
|
|
415
|
+
postMouseClick(button: button, count: count)
|
|
416
|
+
}
|
|
417
|
+
|
|
342
418
|
private func postDrag(from: CGPoint, to: CGPoint) {
|
|
343
|
-
|
|
419
|
+
OverlayController.shared.ensureSession()
|
|
420
|
+
animateOverlay(to: from)
|
|
344
421
|
OverlayController.shared.flashClick()
|
|
345
|
-
Thread.sleep(forTimeInterval: 0.02)
|
|
346
422
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
mouseCursorPosition: from,
|
|
351
|
-
mouseButton: .left
|
|
352
|
-
) {
|
|
353
|
-
down.post(tap: .cghidEventTap)
|
|
354
|
-
}
|
|
423
|
+
withPreservedUserCursor {
|
|
424
|
+
warpSystemCursor(to: from)
|
|
425
|
+
Thread.sleep(forTimeInterval: 0.012)
|
|
355
426
|
|
|
356
|
-
|
|
357
|
-
for i in 1...steps {
|
|
358
|
-
let t = CGFloat(i) / CGFloat(steps)
|
|
359
|
-
let p = CGPoint(
|
|
360
|
-
x: from.x + (to.x - from.x) * t,
|
|
361
|
-
y: from.y + (to.y - from.y) * t
|
|
362
|
-
)
|
|
363
|
-
OverlayController.shared.moveTip(to: p)
|
|
364
|
-
if let drag = CGEvent(
|
|
427
|
+
if let down = CGEvent(
|
|
365
428
|
mouseEventSource: nil,
|
|
366
|
-
mouseType: .
|
|
367
|
-
mouseCursorPosition:
|
|
429
|
+
mouseType: .leftMouseDown,
|
|
430
|
+
mouseCursorPosition: from,
|
|
368
431
|
mouseButton: .left
|
|
369
432
|
) {
|
|
370
|
-
|
|
433
|
+
down.post(tap: .cghidEventTap)
|
|
371
434
|
}
|
|
372
|
-
Thread.sleep(forTimeInterval: 0.008)
|
|
373
|
-
}
|
|
374
435
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
}
|
|
382
464
|
}
|
|
383
465
|
OverlayController.shared.moveTip(to: to)
|
|
384
466
|
}
|
|
385
467
|
|
|
386
468
|
private func postScroll(at point: CGPoint, direction: String, amount: Int) {
|
|
387
|
-
|
|
469
|
+
animateOverlay(to: point)
|
|
388
470
|
let n = max(1, amount)
|
|
389
471
|
var dx = 0
|
|
390
472
|
var dy = 0
|
|
@@ -397,18 +479,21 @@ private func postScroll(at point: CGPoint, direction: String, amount: Int) {
|
|
|
397
479
|
logError("unknown scroll direction: \(direction)")
|
|
398
480
|
return
|
|
399
481
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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)
|
|
410
496
|
}
|
|
411
|
-
Thread.sleep(forTimeInterval: 0.02)
|
|
412
497
|
}
|
|
413
498
|
}
|
|
414
499
|
|
|
@@ -480,7 +565,6 @@ private func postUnicodeChar(_ scalar: UniChar) {
|
|
|
480
565
|
}
|
|
481
566
|
|
|
482
567
|
private func typeText(_ text: String) {
|
|
483
|
-
// Iterate UTF-16 code units; map Return/Tab to virtual keys when present as characters.
|
|
484
568
|
let utf16 = Array(text.utf16)
|
|
485
569
|
var i = 0
|
|
486
570
|
while i < utf16.count {
|
|
@@ -496,7 +580,7 @@ private func typeText(_ text: String) {
|
|
|
496
580
|
postUnicodeChar(unit)
|
|
497
581
|
}
|
|
498
582
|
i += 1
|
|
499
|
-
Thread.sleep(forTimeInterval: 0.
|
|
583
|
+
Thread.sleep(forTimeInterval: 0.002)
|
|
500
584
|
}
|
|
501
585
|
}
|
|
502
586
|
|
|
@@ -547,7 +631,6 @@ private func parseAndPostKey(_ keySpec: String) -> Bool {
|
|
|
547
631
|
}
|
|
548
632
|
}
|
|
549
633
|
|
|
550
|
-
// Function keys F1–F12
|
|
551
634
|
if token.hasPrefix("f"), let n = Int(token.dropFirst()), (1...12).contains(n) {
|
|
552
635
|
let base = kVK_F1
|
|
553
636
|
postKeyCode(CGKeyCode(base + (n - 1)), flags: flags)
|
|
@@ -567,7 +650,6 @@ private func geometryResponse() -> [String: Any] {
|
|
|
567
650
|
}
|
|
568
651
|
let frame = screen.frame
|
|
569
652
|
let scale = screen.backingScaleFactor
|
|
570
|
-
// Primary display: Cocoa origin is bottom-left of main screen; Quartz global top-left origin is (0,0).
|
|
571
653
|
return [
|
|
572
654
|
"ok": true,
|
|
573
655
|
"originX": 0,
|
|
@@ -592,6 +674,33 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
592
674
|
case "ping":
|
|
593
675
|
writeOk()
|
|
594
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
|
+
|
|
595
704
|
case "geometry":
|
|
596
705
|
writeResponse(geometryResponse())
|
|
597
706
|
|
|
@@ -612,7 +721,8 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
612
721
|
writeError("move requires pointX and pointY")
|
|
613
722
|
return
|
|
614
723
|
}
|
|
615
|
-
|
|
724
|
+
// Overlay-only: do not warp the user’s system cursor.
|
|
725
|
+
postOverlayMove(to: CGPoint(x: x, y: y))
|
|
616
726
|
writeOk()
|
|
617
727
|
|
|
618
728
|
case "click":
|
|
@@ -633,6 +743,28 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
633
743
|
postMouseClick(button: button, count: count)
|
|
634
744
|
writeOk()
|
|
635
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
|
+
|
|
636
768
|
case "drag":
|
|
637
769
|
guard let fromX = doubleValue(object, "fromX"),
|
|
638
770
|
let fromY = doubleValue(object, "fromY"),
|
|
@@ -642,7 +774,7 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
642
774
|
writeError("drag requires fromX, fromY, toX, toY")
|
|
643
775
|
return
|
|
644
776
|
}
|
|
645
|
-
postDrag(from:
|
|
777
|
+
postDrag(from: CGPoint(x: fromX, y: fromY), to: CGPoint(x: toX, y: toY))
|
|
646
778
|
writeOk()
|
|
647
779
|
|
|
648
780
|
case "type":
|
|
@@ -678,7 +810,7 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
678
810
|
writeError("direction must be up, down, left, or right")
|
|
679
811
|
return
|
|
680
812
|
}
|
|
681
|
-
postScroll(at:
|
|
813
|
+
postScroll(at: CGPoint(x: x, y: y), direction: dir, amount: amount)
|
|
682
814
|
writeOk()
|
|
683
815
|
|
|
684
816
|
default:
|
|
@@ -698,7 +830,6 @@ private final class StdinReader {
|
|
|
698
830
|
while true {
|
|
699
831
|
let chunk = handle.availableData
|
|
700
832
|
if chunk.isEmpty {
|
|
701
|
-
// EOF — exit the process cleanly.
|
|
702
833
|
DispatchQueue.main.async {
|
|
703
834
|
NSApp.terminate(nil)
|
|
704
835
|
}
|
|
@@ -726,8 +857,45 @@ private final class StdinReader {
|
|
|
726
857
|
}
|
|
727
858
|
}
|
|
728
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
|
+
|
|
729
893
|
private let stdinReader = StdinReader()
|
|
730
894
|
|
|
895
|
+
if CommandLine.arguments.contains("--session-status") {
|
|
896
|
+
printSessionStatusAndExit()
|
|
897
|
+
}
|
|
898
|
+
|
|
731
899
|
autoreleasepool {
|
|
732
900
|
let app = NSApplication.shared
|
|
733
901
|
app.setActivationPolicy(.accessory)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jian-ai/remote-control",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Jian Remote Control worker — connect a computer to a Jian gateway over HTTPS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
15
|
"macos",
|
|
16
|
+
"linux",
|
|
17
|
+
"windows",
|
|
16
18
|
"README.md"
|
|
17
19
|
],
|
|
18
20
|
"engines": {
|