ucu-mcp 0.5.2 → 0.6.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/CHANGELOG.md +68 -0
- package/dist/src/mcp/tools/app-tools.js +7 -0
- package/dist/src/mcp/tools/helpers.js +32 -22
- package/dist/src/mcp/tools/input-tools.js +16 -10
- package/dist/src/mcp/tools/keyboard-tools.js +8 -4
- package/dist/src/mcp/tools/screen-tools.js +85 -53
- package/dist/src/platform/base.d.ts +17 -6
- package/dist/src/platform/macos/base.d.ts +12 -3
- package/dist/src/platform/macos/base.js +10 -2
- package/dist/src/platform/macos/element.d.ts +2 -0
- package/dist/src/platform/macos/element.js +67 -68
- package/dist/src/platform/macos/helpers.js +10 -5
- package/dist/src/platform/macos/input.d.ts +7 -6
- package/dist/src/platform/macos/input.js +31 -7
- package/dist/src/platform/macos/screen.js +10 -2
- package/dist/src/platform/macos/window.d.ts +2 -0
- package/dist/src/platform/macos/window.js +49 -26
- package/dist/src/safety/permissions.d.ts +2 -0
- package/dist/src/safety/permissions.js +34 -2
- package/dist/src/utils/input.d.ts +14 -8
- package/dist/src/utils/input.js +102 -43
- package/native/skylight/main.swift +402 -0
- package/native/skylight/skylight-helper +0 -0
- package/package.json +2 -2
- package/skills/ucu-mcp/SKILL.md +17 -0
package/dist/src/utils/input.js
CHANGED
|
@@ -18,33 +18,93 @@ const execFileAsync = promisify(execFile);
|
|
|
18
18
|
// ── Native CGEvent helper (macOS) ──────────────────────────────────────
|
|
19
19
|
// JXA (osascript -l JavaScript) cannot call CGEventPost without segfault.
|
|
20
20
|
// We ship a small Swift binary that does native CGEvent injection instead.
|
|
21
|
+
//
|
|
22
|
+
// v0.6.0: two helpers. `skylight-helper` posts events per-process via
|
|
23
|
+
// SLEventPostToPid (private SkyLight SPI) so the global cursor does NOT move
|
|
24
|
+
// and the foreground is NOT stolen — matching Codex computer-use. `cgevent-helper`
|
|
25
|
+
// (legacy) posts to the HID event tap, which moves the cursor. We prefer
|
|
26
|
+
// skylight when a target pid is available; fall back to cgevent otherwise.
|
|
21
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
28
|
// In dev: src/utils/input.ts → native/cgevent/cgevent-helper
|
|
23
29
|
// In prod: dist/src/utils/input.js → dist/native/cgevent/cgevent-helper
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
30
|
+
const cgeventHelperPath = join(__dirname, "..", "..", "..", "native", "cgevent", "cgevent-helper");
|
|
31
|
+
const cgeventHelperPathAlt = join(__dirname, "..", "..", "native", "cgevent", "cgevent-helper");
|
|
32
|
+
const skylightHelperPath = join(__dirname, "..", "..", "..", "native", "skylight", "skylight-helper");
|
|
33
|
+
const skylightHelperPathAlt = join(__dirname, "..", "..", "native", "skylight", "skylight-helper");
|
|
27
34
|
import { existsSync } from "node:fs";
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const resolvedCgeventPath = existsSync(cgeventHelperPath) ? cgeventHelperPath : cgeventHelperPathAlt;
|
|
36
|
+
const resolvedSkylightPath = existsSync(skylightHelperPath) ? skylightHelperPath : skylightHelperPathAlt;
|
|
37
|
+
let _cgeventAvailable;
|
|
38
|
+
let _skylightAvailable;
|
|
39
|
+
function isCgeventAvailable() {
|
|
40
|
+
if (_cgeventAvailable !== undefined)
|
|
41
|
+
return _cgeventAvailable;
|
|
33
42
|
try {
|
|
34
|
-
const stdout = execFileSync(
|
|
43
|
+
const stdout = execFileSync(resolvedCgeventPath, [], {
|
|
35
44
|
input: '{"command":"ping"}',
|
|
36
45
|
encoding: "utf8",
|
|
37
46
|
timeout: 3000,
|
|
38
47
|
});
|
|
39
|
-
|
|
48
|
+
_cgeventAvailable = stdout.includes('"ok"');
|
|
40
49
|
}
|
|
41
50
|
catch {
|
|
42
|
-
|
|
51
|
+
_cgeventAvailable = false;
|
|
43
52
|
}
|
|
44
|
-
return
|
|
53
|
+
return _cgeventAvailable;
|
|
45
54
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
55
|
+
function isSkylightAvailable() {
|
|
56
|
+
if (_skylightAvailable !== undefined)
|
|
57
|
+
return _skylightAvailable;
|
|
58
|
+
if (!existsSync(resolvedSkylightPath)) {
|
|
59
|
+
_skylightAvailable = false;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const stdout = execFileSync(resolvedSkylightPath, [], {
|
|
64
|
+
input: '{"command":"ping"}',
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 3000,
|
|
67
|
+
});
|
|
68
|
+
_skylightAvailable = stdout.includes('"ok"');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
_skylightAvailable = false;
|
|
72
|
+
}
|
|
73
|
+
return _skylightAvailable;
|
|
74
|
+
}
|
|
75
|
+
/** @deprecated use isCgeventAvailable — kept for external callers/tests. */
|
|
76
|
+
function isNativeAvailable() {
|
|
77
|
+
return isCgeventAvailable();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run an input command via the best available helper.
|
|
81
|
+
* - target.pid > 0 AND skylight available → skylight-helper (per-process, no cursor move). Returns "per-pid".
|
|
82
|
+
* - otherwise → cgevent-helper (HID tap, moves cursor). Returns "hid-tap".
|
|
83
|
+
* If skylight is chosen but errors at runtime, falls back to cgevent (HID-tap) rather than throwing.
|
|
84
|
+
* Throws only if BOTH helpers fail.
|
|
85
|
+
*/
|
|
86
|
+
function runInputChecked(payload, target) {
|
|
87
|
+
const useSkylight = !!target?.pid && target.pid > 0 && isSkylightAvailable();
|
|
88
|
+
if (useSkylight) {
|
|
89
|
+
const fullPayload = { ...payload, pid: target.pid, windowNumber: target.windowNumber };
|
|
90
|
+
try {
|
|
91
|
+
const raw = execFileSync(resolvedSkylightPath, [], {
|
|
92
|
+
input: JSON.stringify(fullPayload),
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
timeout: 10000,
|
|
95
|
+
}).trim();
|
|
96
|
+
const resp = JSON.parse(raw);
|
|
97
|
+
if (!resp.error) {
|
|
98
|
+
return (resp.method === "per-pid") ? "per-pid" : "hid-tap";
|
|
99
|
+
}
|
|
100
|
+
// skylight errored at runtime (SPI filtered / event build failed) → fall through to cgevent.
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// skylight crashed/timed out → fall through to cgevent.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// cgevent path (HID-tap) — the reliable fallback.
|
|
107
|
+
const raw = execFileSync(resolvedCgeventPath, [], {
|
|
48
108
|
input: JSON.stringify(payload),
|
|
49
109
|
encoding: "utf8",
|
|
50
110
|
timeout: 10000,
|
|
@@ -53,6 +113,11 @@ function runNativeChecked(payload) {
|
|
|
53
113
|
if (resp.error) {
|
|
54
114
|
throw new Error(`native helper error: ${resp.error}`);
|
|
55
115
|
}
|
|
116
|
+
return "hid-tap";
|
|
117
|
+
}
|
|
118
|
+
/** @deprecated use runInputChecked — kept for external callers/tests. */
|
|
119
|
+
function runNativeChecked(payload) {
|
|
120
|
+
runInputChecked(payload);
|
|
56
121
|
}
|
|
57
122
|
// ── Dry-run mode ──────────────────────────────────────────────────────────
|
|
58
123
|
const isDryRun = () => process.env.UCU_DRY_RUN === "true";
|
|
@@ -112,15 +177,14 @@ async function runJXA(script, timeout = 5000) {
|
|
|
112
177
|
return stdout.trim();
|
|
113
178
|
}
|
|
114
179
|
// ── Mouse operations (CGEvent — background, no focus steal) ───────────────
|
|
115
|
-
export async function click(x, y, button = "left", _platform = process.platform) {
|
|
180
|
+
export async function click(x, y, button = "left", _platform = process.platform, target) {
|
|
116
181
|
if (isDryRun()) {
|
|
117
182
|
logDryRun("click", { x, y, button });
|
|
118
183
|
return;
|
|
119
184
|
}
|
|
120
185
|
if (_platform === "darwin") {
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
return;
|
|
186
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
187
|
+
return runInputChecked({ command: "click", x, y, button }, target);
|
|
124
188
|
}
|
|
125
189
|
const btnType = { left: 0, right: 1, middle: 2 }[button];
|
|
126
190
|
await runJXA(`
|
|
@@ -144,15 +208,14 @@ export async function click(x, y, button = "left", _platform = process.platform)
|
|
|
144
208
|
// Windows
|
|
145
209
|
throw new Error("click not implemented for Windows");
|
|
146
210
|
}
|
|
147
|
-
export async function doubleClick(x, y, button = "left", _platform = process.platform) {
|
|
211
|
+
export async function doubleClick(x, y, button = "left", _platform = process.platform, target) {
|
|
148
212
|
if (isDryRun()) {
|
|
149
213
|
logDryRun("doubleClick", { x, y, button });
|
|
150
214
|
return;
|
|
151
215
|
}
|
|
152
216
|
if (_platform === "darwin") {
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
return;
|
|
217
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
218
|
+
return runInputChecked({ command: "doubleClick", x, y, button }, target);
|
|
156
219
|
}
|
|
157
220
|
const btnType = { left: 0, right: 1, middle: 2 }[button];
|
|
158
221
|
await runJXA(`
|
|
@@ -182,15 +245,14 @@ export async function doubleClick(x, y, button = "left", _platform = process.pla
|
|
|
182
245
|
await new Promise(r => setTimeout(r, 50));
|
|
183
246
|
await click(x, y, button, _platform);
|
|
184
247
|
}
|
|
185
|
-
export async function move(x, y, _platform = process.platform) {
|
|
248
|
+
export async function move(x, y, _platform = process.platform, target) {
|
|
186
249
|
if (isDryRun()) {
|
|
187
250
|
logDryRun("move", { x, y });
|
|
188
251
|
return;
|
|
189
252
|
}
|
|
190
253
|
if (_platform === "darwin") {
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
return;
|
|
254
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
255
|
+
return runInputChecked({ command: "move", x, y }, target);
|
|
194
256
|
}
|
|
195
257
|
await runJXA(`
|
|
196
258
|
ObjC.import('CoreGraphics');
|
|
@@ -207,15 +269,14 @@ export async function move(x, y, _platform = process.platform) {
|
|
|
207
269
|
}
|
|
208
270
|
throw new Error("move not implemented for Windows");
|
|
209
271
|
}
|
|
210
|
-
export async function drag(fromX, fromY, toX, toY, button = "left", duration = 300, _platform = process.platform) {
|
|
272
|
+
export async function drag(fromX, fromY, toX, toY, button = "left", duration = 300, _platform = process.platform, target) {
|
|
211
273
|
if (isDryRun()) {
|
|
212
274
|
logDryRun("drag", { fromX, fromY, toX, toY, button, duration });
|
|
213
275
|
return;
|
|
214
276
|
}
|
|
215
277
|
if (_platform === "darwin") {
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
return;
|
|
278
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
279
|
+
return runInputChecked({ command: "drag", fromX, fromY, toX, toY, button, durationMs: duration }, target);
|
|
219
280
|
}
|
|
220
281
|
const btnType = { left: 0, right: 1, middle: 2 }[button];
|
|
221
282
|
const steps = Math.max(2, Math.min(60, Math.ceil(duration / 16)));
|
|
@@ -257,15 +318,14 @@ export async function drag(fromX, fromY, toX, toY, button = "left", duration = 3
|
|
|
257
318
|
}
|
|
258
319
|
throw new Error("drag not implemented for Windows");
|
|
259
320
|
}
|
|
260
|
-
export async function scroll(x, y, deltaX, deltaY, _platform = process.platform) {
|
|
321
|
+
export async function scroll(x, y, deltaX, deltaY, _platform = process.platform, target) {
|
|
261
322
|
if (isDryRun()) {
|
|
262
323
|
logDryRun("scroll", { x, y, deltaX, deltaY });
|
|
263
324
|
return;
|
|
264
325
|
}
|
|
265
326
|
if (_platform === "darwin") {
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
return;
|
|
327
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
328
|
+
return runInputChecked({ command: "scroll", x, y, deltaX, deltaY }, target);
|
|
269
329
|
}
|
|
270
330
|
const verticalDelta = -deltaY;
|
|
271
331
|
const horizontalDelta = deltaX;
|
|
@@ -292,7 +352,7 @@ export async function scroll(x, y, deltaX, deltaY, _platform = process.platform)
|
|
|
292
352
|
throw new Error("scroll not implemented for Windows");
|
|
293
353
|
}
|
|
294
354
|
// ── Keyboard operations (CGEvent — background) ────────────────────────────
|
|
295
|
-
export async function typeText(text, delay = 20, _platform = process.platform) {
|
|
355
|
+
export async function typeText(text, delay = 20, _platform = process.platform, target) {
|
|
296
356
|
if (isDryRun()) {
|
|
297
357
|
logDryRun("typeText", { text: text.slice(0, 50), delay });
|
|
298
358
|
return;
|
|
@@ -377,8 +437,8 @@ export async function typeText(text, delay = 20, _platform = process.platform) {
|
|
|
377
437
|
// Process each batch
|
|
378
438
|
for (const batch of batches) {
|
|
379
439
|
if (batch.cgEvent && Array.isArray(batch.chars)) {
|
|
380
|
-
if (
|
|
381
|
-
|
|
440
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
441
|
+
runInputChecked({ command: "typeBatch", keys: batch.chars }, target);
|
|
382
442
|
}
|
|
383
443
|
else {
|
|
384
444
|
// Build a single JXA script that types all chars in this CGEvent batch
|
|
@@ -418,7 +478,7 @@ export async function typeText(text, delay = 20, _platform = process.platform) {
|
|
|
418
478
|
}
|
|
419
479
|
throw new Error("typeText not implemented for Windows");
|
|
420
480
|
}
|
|
421
|
-
export async function pressKey(key, modifiers = [], _platform = process.platform) {
|
|
481
|
+
export async function pressKey(key, modifiers = [], _platform = process.platform, target) {
|
|
422
482
|
if (isDryRun()) {
|
|
423
483
|
logDryRun("pressKey", { key, modifiers });
|
|
424
484
|
return;
|
|
@@ -443,9 +503,8 @@ export async function pressKey(key, modifiers = [], _platform = process.platform
|
|
|
443
503
|
}
|
|
444
504
|
flags |= flag;
|
|
445
505
|
}
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
return;
|
|
506
|
+
if (isCgeventAvailable() || isSkylightAvailable()) {
|
|
507
|
+
return runInputChecked({ command: "pressKey", keyCode, flags }, target);
|
|
449
508
|
}
|
|
450
509
|
await runJXA(`
|
|
451
510
|
ObjC.import('CoreGraphics');
|
|
@@ -468,7 +527,7 @@ export async function pressKey(key, modifiers = [], _platform = process.platform
|
|
|
468
527
|
}
|
|
469
528
|
throw new Error("pressKey not implemented for Windows");
|
|
470
529
|
}
|
|
471
|
-
export async function pressShortcut(keys, _platform = process.platform) {
|
|
530
|
+
export async function pressShortcut(keys, _platform = process.platform, target) {
|
|
472
531
|
if (isDryRun()) {
|
|
473
532
|
logDryRun("pressShortcut", { keys });
|
|
474
533
|
return;
|
|
@@ -478,7 +537,7 @@ export async function pressShortcut(keys, _platform = process.platform) {
|
|
|
478
537
|
}
|
|
479
538
|
const modifiers = keys.slice(0, -1);
|
|
480
539
|
const key = keys[keys.length - 1];
|
|
481
|
-
await pressKey(key, modifiers, _platform);
|
|
540
|
+
return await pressKey(key, modifiers, _platform, target);
|
|
482
541
|
}
|
|
483
542
|
// ── Cursor position ───────────────────────────────────────────────────────
|
|
484
543
|
export async function getCursorPosition(_platform = process.platform) {
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// skylight-helper: per-process event posting via private SkyLight SPIs.
|
|
2
|
+
//
|
|
3
|
+
// Posts mouse/keyboard events to a SPECIFIC process (by pid) without moving the
|
|
4
|
+
// global cursor or stealing foreground — matching Codex computer-use behavior.
|
|
5
|
+
// Uses SLEventPostToPid (private SkyLight) for mouse, CGEventPostToPid (public)
|
|
6
|
+
// for keyboard. Falls back to HID-tap posting (which moves the cursor) when the
|
|
7
|
+
// target is frontmost/canvas-app or when SPIs are unavailable.
|
|
8
|
+
//
|
|
9
|
+
// stdin/stdout JSON protocol mirrors cgevent-helper (line-oriented).
|
|
10
|
+
|
|
11
|
+
import CoreGraphics
|
|
12
|
+
import Foundation
|
|
13
|
+
import AppKit
|
|
14
|
+
|
|
15
|
+
// MARK: - SkyLight SPI loading
|
|
16
|
+
|
|
17
|
+
private let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)!
|
|
18
|
+
|
|
19
|
+
// Force-load SkyLight so its symbols register in the global namespace.
|
|
20
|
+
_ = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY)
|
|
21
|
+
|
|
22
|
+
private func sym<T>(_ name: String, as _: T.Type) -> T? {
|
|
23
|
+
guard let p = dlsym(RTLD_DEFAULT, name) else { return nil }
|
|
24
|
+
return unsafeBitCast(p, to: T.self)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// void SLEventPostToPid(pid_t, CGEventRef)
|
|
28
|
+
private typealias SLEventPostToPidFn = @convention(c) (pid_t, CGEvent) -> Void
|
|
29
|
+
private let slPostToPid: SLEventPostToPidFn? = sym("SLEventPostToPid", as: SLEventPostToPidFn.self)
|
|
30
|
+
|
|
31
|
+
// void CGEventSetWindowLocation(CGEventRef, CGPoint) — private
|
|
32
|
+
private typealias SetWindowLocFn = @convention(c) (CGEvent, CGPoint) -> Void
|
|
33
|
+
private let setWindowLoc: SetWindowLocFn? = sym("CGEventSetWindowLocation", as: SetWindowLocFn.self)
|
|
34
|
+
|
|
35
|
+
// void SLEventSetIntegerValueField(CGEventRef, uint32_t, int64_t) — private
|
|
36
|
+
private typealias SetIntFieldFn = @convention(c) (CGEvent, UInt32, Int64) -> Void
|
|
37
|
+
private let setIntField: SetIntFieldFn? = sym("SLEventSetIntegerValueField", as: SetIntFieldFn.self)
|
|
38
|
+
|
|
39
|
+
// CGSConnectionID CGSMainConnectionID(void)
|
|
40
|
+
private typealias ConnIDFn = @convention(c) () -> UInt32
|
|
41
|
+
private let mainConnID: ConnIDFn? = sym("CGSMainConnectionID", as: ConnIDFn.self)
|
|
42
|
+
|
|
43
|
+
// CGError SLSGetWindowOwner(CGSConnectionID, CGWindowID, CGSConnectionID*)
|
|
44
|
+
private typealias GetWindowOwnerFn = @convention(c) (UInt32, UInt32, UnsafeMutablePointer<UInt32>) -> Int32
|
|
45
|
+
private let getWindowOwner: GetWindowOwnerFn? = sym("SLSGetWindowOwner", as: GetWindowOwnerFn.self)
|
|
46
|
+
|
|
47
|
+
// OSStatus SLSGetConnectionPSN(CGSConnectionID, ProcessSerialNumber*)
|
|
48
|
+
private typealias GetConnPSNFn = @convention(c) (UInt32, UnsafeMutableRawPointer) -> Int32
|
|
49
|
+
private let getConnPSN: GetConnPSNFn? = sym("SLSGetConnectionPSN", as: GetConnPSNFn.self)
|
|
50
|
+
|
|
51
|
+
// OSStatus _SLPSGetFrontProcess(ProcessSerialNumber*)
|
|
52
|
+
private typealias GetFrontPSNFn = @convention(c) (UnsafeMutableRawPointer) -> Int32
|
|
53
|
+
private let getFrontPSN: GetFrontPSNFn? = sym("_SLPSGetFrontProcess", as: GetFrontPSNFn.self)
|
|
54
|
+
|
|
55
|
+
// OSStatus SLPSPostEventRecordTo(ProcessSerialNumber*, uint8_t*)
|
|
56
|
+
private typealias PostEventRecFn = @convention(c) (UnsafeRawPointer, UnsafePointer<UInt8>) -> Int32
|
|
57
|
+
private let postEventRec: PostEventRecFn? = sym("SLPSPostEventRecordTo", as: PostEventRecFn.self)
|
|
58
|
+
|
|
59
|
+
/// True if all SPIs needed for per-process mouse posting are present.
|
|
60
|
+
private let skylightMouseAvailable: Bool = (slPostToPid != nil)
|
|
61
|
+
|
|
62
|
+
// MARK: - I/O helpers
|
|
63
|
+
|
|
64
|
+
struct Input: Decodable {
|
|
65
|
+
let command: String
|
|
66
|
+
let pid: Int32?
|
|
67
|
+
let windowNumber: Int?
|
|
68
|
+
let x: Double?
|
|
69
|
+
let y: Double?
|
|
70
|
+
let fromX: Double?
|
|
71
|
+
let fromY: Double?
|
|
72
|
+
let toX: Double?
|
|
73
|
+
let toY: Double?
|
|
74
|
+
let button: String?
|
|
75
|
+
let durationMs: Int?
|
|
76
|
+
let deltaX: Int?
|
|
77
|
+
let deltaY: Int?
|
|
78
|
+
let keyCode: Int?
|
|
79
|
+
let flags: Int64?
|
|
80
|
+
let keys: [KeyEntry]?
|
|
81
|
+
struct KeyEntry: Decodable { let code: Int; let shift: Bool? }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func out(_ json: String) {
|
|
85
|
+
FileHandle.standardOutput.write((json + "\n").data(using: .utf8)!)
|
|
86
|
+
fflush(stdout)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func btn(_ s: String?) -> CGMouseButton {
|
|
90
|
+
switch s { case "right": return .right; case "middle": return .center; default: return .left }
|
|
91
|
+
}
|
|
92
|
+
func downT(_ b: CGMouseButton) -> CGEventType {
|
|
93
|
+
switch b { case .right: return .rightMouseDown; case .center: return .otherMouseDown; default: return .leftMouseDown }
|
|
94
|
+
}
|
|
95
|
+
func upT(_ b: CGMouseButton) -> CGEventType {
|
|
96
|
+
switch b { case .right: return .rightMouseUp; case .center: return .otherMouseUp; default: return .leftMouseUp }
|
|
97
|
+
}
|
|
98
|
+
func dragT(_ b: CGMouseButton) -> CGEventType {
|
|
99
|
+
switch b { case .right: return .rightMouseDragged; case .center: return .otherMouseDragged; default: return .leftMouseDragged }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - Event construction & posting
|
|
103
|
+
|
|
104
|
+
/// Build a mouse CGEvent via the NSEvent bridge (raw CGEventCreateMouseEvent events are
|
|
105
|
+
/// filtered by Chromium's renderer IPC). Returns nil on failure.
|
|
106
|
+
func makeMouseEvent(_ type: CGEventType, at point: CGPoint, button: CGMouseButton,
|
|
107
|
+
clickCount: Int = 1, windowNumber: Int = 0) -> CGEvent? {
|
|
108
|
+
let nsType: NSEvent.EventType
|
|
109
|
+
switch type {
|
|
110
|
+
case .leftMouseDown: nsType = .leftMouseDown
|
|
111
|
+
case .leftMouseUp: nsType = .leftMouseUp
|
|
112
|
+
case .rightMouseDown: nsType = .rightMouseDown
|
|
113
|
+
case .rightMouseUp: nsType = .rightMouseUp
|
|
114
|
+
case .otherMouseDown: nsType = .otherMouseDown
|
|
115
|
+
case .otherMouseUp: nsType = .otherMouseUp
|
|
116
|
+
case .leftMouseDragged: nsType = .leftMouseDragged
|
|
117
|
+
case .rightMouseDragged: nsType = .rightMouseDragged
|
|
118
|
+
case .otherMouseDragged: nsType = .otherMouseDragged
|
|
119
|
+
case .mouseMoved: nsType = .mouseMoved
|
|
120
|
+
default: return nil
|
|
121
|
+
}
|
|
122
|
+
guard let ns = NSEvent.mouseEvent(
|
|
123
|
+
with: nsType, location: point, modifierFlags: [],
|
|
124
|
+
timestamp: 0, windowNumber: windowNumber, context: nil,
|
|
125
|
+
eventNumber: 0, clickCount: clickCount, pressure: (type == .mouseMoved) ? 0 : 1.0
|
|
126
|
+
), let cg = ns.cgEvent else { return nil }
|
|
127
|
+
return cg
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Stamp a mouse event with the fields Chromium's synthetic-event filter requires,
|
|
131
|
+
/// then post it to the target pid via SLEventPostToPid (no global cursor move).
|
|
132
|
+
func postPerPid(_ event: CGEvent, pid: Int32, at point: CGPoint, windowNumber: Int, button: CGMouseButton = .left) {
|
|
133
|
+
event.location = point
|
|
134
|
+
// Stamp the correct button number (0=left, 1=right, 2=center) — NOT hardcoded 0.
|
|
135
|
+
event.setIntegerValueField(.mouseEventButtonNumber, value: Int64(button.rawValue))
|
|
136
|
+
event.setIntegerValueField(.mouseEventSubtype, value: 3)
|
|
137
|
+
if windowNumber > 0 {
|
|
138
|
+
event.setIntegerValueField(.mouseEventWindowUnderMousePointer, value: Int64(windowNumber))
|
|
139
|
+
event.setIntegerValueField(.mouseEventWindowUnderMousePointerThatCanHandleThisEvent, value: Int64(windowNumber))
|
|
140
|
+
}
|
|
141
|
+
// Private SPIs: window-local point + Chromium pid latch (field 40).
|
|
142
|
+
setWindowLoc?(event, point)
|
|
143
|
+
setIntField?(event, 40, Int64(pid))
|
|
144
|
+
event.timestamp = clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
|
|
145
|
+
slPostToPid?(pid, event)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// HID-tap fallback (moves the cursor). Used for frontmost/canvas apps where
|
|
149
|
+
/// per-pid posting is filtered.
|
|
150
|
+
func postHidTap(_ event: CGEvent) {
|
|
151
|
+
event.post(tap: CGEventTapLocation.cghidEventTap)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// True if the target pid is the frontmost app (canvas/GPU apps filter per-pid
|
|
155
|
+
/// routes — must use HID-tap there).
|
|
156
|
+
func isFrontmost(_ pid: Int32) -> Bool {
|
|
157
|
+
return NSRunningApplication(processIdentifier: pid)?.isActive == true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// MARK: - Focus-without-raise (SLPSPostEventRecordTo)
|
|
161
|
+
|
|
162
|
+
/// Flip the target's AppKit-active state without raising its window (no Space switch).
|
|
163
|
+
func focusWithoutRaise(pid: Int32, windowID: Int) {
|
|
164
|
+
guard let postRec = postEventRec, let frontFn = getFrontPSN else { return }
|
|
165
|
+
// Resolve target PSN via window owner.
|
|
166
|
+
guard let connFn = mainConnID, let ownerFn = getWindowOwner, let psnFn = getConnPSN,
|
|
167
|
+
windowID > 0 else { return }
|
|
168
|
+
var prevPSN = [UInt32](repeating: 0, count: 2)
|
|
169
|
+
var targetPSN = [UInt32](repeating: 0, count: 2)
|
|
170
|
+
let prevOk = prevPSN.withUnsafeMutableBytes { raw in frontFn(raw.baseAddress!) != 0 }
|
|
171
|
+
if prevOk {
|
|
172
|
+
let cid = connFn()
|
|
173
|
+
var ownerCid: UInt32 = 0
|
|
174
|
+
guard ownerFn(cid, UInt32(windowID), &ownerCid) == 0 else { return }
|
|
175
|
+
guard psnFn(ownerCid, &targetPSN) == 0 else { return }
|
|
176
|
+
} else {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
var buf = [UInt8](repeating: 0, count: 0xF8) // 248 bytes
|
|
180
|
+
buf[0x04] = 0xF8
|
|
181
|
+
buf[0x08] = 0x0D
|
|
182
|
+
let wid = UInt32(windowID)
|
|
183
|
+
buf[0x3C] = UInt8(wid & 0xFF); buf[0x3D] = UInt8((wid >> 8) & 0xFF)
|
|
184
|
+
buf[0x3E] = UInt8((wid >> 16) & 0xFF); buf[0x3F] = UInt8((wid >> 24) & 0xFF)
|
|
185
|
+
// Defocus previous front.
|
|
186
|
+
buf[0x8A] = 0x02
|
|
187
|
+
_ = prevPSN.withUnsafeBytes { psnRaw in buf.withUnsafeBufferPointer { bp in postRec(psnRaw.baseAddress!, bp.baseAddress!) } }
|
|
188
|
+
// Focus target.
|
|
189
|
+
buf[0x8A] = 0x01
|
|
190
|
+
_ = targetPSN.withUnsafeBytes { psnRaw in buf.withUnsafeBufferPointer { bp in postRec(psnRaw.baseAddress!, bp.baseAddress!) } }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// MARK: - AX keepalive (root-cause fix for Electron/Tauri AXPress silent failure)
|
|
194
|
+
|
|
195
|
+
/// Keep the target app's AX tree alive when occluded/background.
|
|
196
|
+
///
|
|
197
|
+
/// Writes the AXManualAccessibility attribute (persists for the app's lifetime,
|
|
198
|
+
/// independent of this helper process). AXEnhancedUserInterface is NOT set
|
|
199
|
+
/// unconditionally — it's known to slow/break some apps (Xcode, Slack, Office);
|
|
200
|
+
/// gate it behind the UCU_AX_ENHANCED env var if needed.
|
|
201
|
+
///
|
|
202
|
+
/// The observer-based remote-aware keepalive (_AXObserverAddNotificationAndCheckRemote)
|
|
203
|
+
/// requires a long-lived process to hold the subscription; this helper is one-shot
|
|
204
|
+
/// (readLine → exit), so the observer would be dropped on exit. We therefore rely on
|
|
205
|
+
/// the persistent attribute writes, which cover non-Chromium occluded apps. Full
|
|
206
|
+
/// Chromium keepalive would require a daemon helper (future work).
|
|
207
|
+
func doKeepAlive(_ p: Input) -> String {
|
|
208
|
+
guard let pid = p.pid, pid > 0 else { return "{\"error\":\"no pid\"}" }
|
|
209
|
+
let app = AXUIElementCreateApplication(pid)
|
|
210
|
+
// AXManualAccessibility: safe, persistent, tells the app to expose its AX tree.
|
|
211
|
+
let manualStatus = AXUIElementSetAttributeValue(app, "AXManualAccessibility" as CFString, kCFBooleanTrue!)
|
|
212
|
+
// AXEnhancedUserInterface: opt-in only (breaks some apps when unconditional).
|
|
213
|
+
var enhancedStatus: AXError = .cannotComplete
|
|
214
|
+
if ProcessInfo.processInfo.environment["UCU_AX_ENHANCED"] != nil {
|
|
215
|
+
enhancedStatus = AXUIElementSetAttributeValue(app, "AXEnhancedUserInterface" as CFString, kCFBooleanTrue!)
|
|
216
|
+
}
|
|
217
|
+
let ok = (manualStatus == .success)
|
|
218
|
+
return "{\"ok\":\(ok),\"method\":\"ax-keepalive\",\"manual\":\(manualStatus == .success),\"enhanced\":\(enhancedStatus == .success)}"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// MARK: - Commands
|
|
222
|
+
|
|
223
|
+
func doClick(_ p: Input) -> String {
|
|
224
|
+
guard let pid = p.pid, pid > 0 else { return "{\"error\":\"no pid\"}" }
|
|
225
|
+
let loc = CGPoint(x: p.x ?? 0, y: p.y ?? 0)
|
|
226
|
+
let b = btn(p.button)
|
|
227
|
+
let winNum = p.windowNumber ?? 0
|
|
228
|
+
// Canvas/frontmost apps filter per-pid routes → HID-tap fallback.
|
|
229
|
+
if !skylightMouseAvailable || isFrontmost(pid) {
|
|
230
|
+
guard let dn = CGEvent(mouseEventSource: nil, mouseType: downT(b), mouseCursorPosition: loc, mouseButton: b),
|
|
231
|
+
let up = CGEvent(mouseEventSource: nil, mouseType: upT(b), mouseCursorPosition: loc, mouseButton: b)
|
|
232
|
+
else { return "{\"error\":\"fail\"}" }
|
|
233
|
+
postHidTap(dn); postHidTap(up)
|
|
234
|
+
return "{\"ok\":true,\"method\":\"hid-tap\"}"
|
|
235
|
+
}
|
|
236
|
+
focusWithoutRaise(pid: pid, windowID: winNum)
|
|
237
|
+
// Leading mouseMoved at target (cursor-state primer).
|
|
238
|
+
if let mv = makeMouseEvent(.mouseMoved, at: loc, button: b, windowNumber: winNum) {
|
|
239
|
+
postPerPid(mv, pid: pid, at: loc, windowNumber: winNum, button: b); usleep(15_000)
|
|
240
|
+
}
|
|
241
|
+
// Off-screen primer click @ (-1,-1) — satisfies Chromium user-activation gate.
|
|
242
|
+
let off = CGPoint(x: -1, y: -1)
|
|
243
|
+
if let pd = makeMouseEvent(.leftMouseDown, at: off, button: .left, windowNumber: winNum),
|
|
244
|
+
let pu = makeMouseEvent(.leftMouseUp, at: off, button: .left, windowNumber: winNum) {
|
|
245
|
+
postPerPid(pd, pid: pid, at: off, windowNumber: winNum); usleep(1_000)
|
|
246
|
+
postPerPid(pu, pid: pid, at: off, windowNumber: winNum); usleep(100_000)
|
|
247
|
+
}
|
|
248
|
+
// Target click.
|
|
249
|
+
guard let dn = makeMouseEvent(downT(b), at: loc, button: b, windowNumber: winNum),
|
|
250
|
+
let up = makeMouseEvent(upT(b), at: loc, button: b, windowNumber: winNum)
|
|
251
|
+
else { return "{\"error\":\"fail\"}" }
|
|
252
|
+
postPerPid(dn, pid: pid, at: loc, windowNumber: winNum, button: b); usleep(1_000)
|
|
253
|
+
postPerPid(up, pid: pid, at: loc, windowNumber: winNum, button: b)
|
|
254
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func doDoubleClick(_ p: Input) -> String {
|
|
258
|
+
guard let pid = p.pid, pid > 0 else { return "{\"error\":\"no pid\"}" }
|
|
259
|
+
let loc = CGPoint(x: p.x ?? 0, y: p.y ?? 0)
|
|
260
|
+
let b = btn(p.button)
|
|
261
|
+
let winNum = p.windowNumber ?? 0
|
|
262
|
+
if !skylightMouseAvailable || isFrontmost(pid) {
|
|
263
|
+
return hidTapDouble(loc: loc, b: b)
|
|
264
|
+
}
|
|
265
|
+
focusWithoutRaise(pid: pid, windowID: winNum)
|
|
266
|
+
for state in [1, 2] {
|
|
267
|
+
guard let dn = makeMouseEvent(downT(b), at: loc, button: b, clickCount: state, windowNumber: winNum),
|
|
268
|
+
let up = makeMouseEvent(upT(b), at: loc, button: b, clickCount: state, windowNumber: winNum)
|
|
269
|
+
else { return "{\"error\":\"fail\"}" }
|
|
270
|
+
postPerPid(dn, pid: pid, at: loc, windowNumber: winNum, button: b); usleep(1_000)
|
|
271
|
+
postPerPid(up, pid: pid, at: loc, windowNumber: winNum, button: b)
|
|
272
|
+
if state == 1 { usleep(80_000) }
|
|
273
|
+
}
|
|
274
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func hidTapDouble(loc: CGPoint, b: CGMouseButton) -> String {
|
|
278
|
+
guard let d1 = CGEvent(mouseEventSource: nil, mouseType: downT(b), mouseCursorPosition: loc, mouseButton: b),
|
|
279
|
+
let u1 = CGEvent(mouseEventSource: nil, mouseType: upT(b), mouseCursorPosition: loc, mouseButton: b),
|
|
280
|
+
let d2 = CGEvent(mouseEventSource: nil, mouseType: downT(b), mouseCursorPosition: loc, mouseButton: b),
|
|
281
|
+
let u2 = CGEvent(mouseEventSource: nil, mouseType: upT(b), mouseCursorPosition: loc, mouseButton: b)
|
|
282
|
+
else { return "{\"error\":\"fail\"}" }
|
|
283
|
+
d1.setIntegerValueField(.mouseEventClickState, value: 1); u1.setIntegerValueField(.mouseEventClickState, value: 1)
|
|
284
|
+
d2.setIntegerValueField(.mouseEventClickState, value: 2); u2.setIntegerValueField(.mouseEventClickState, value: 2)
|
|
285
|
+
postHidTap(d1); postHidTap(u1); postHidTap(d2); postHidTap(u2)
|
|
286
|
+
return "{\"ok\":true,\"method\":\"hid-tap\"}"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func doMove(_ p: Input) -> String {
|
|
290
|
+
guard let pid = p.pid, pid > 0, skylightMouseAvailable, !isFrontmost(pid) else {
|
|
291
|
+
let loc = CGPoint(x: p.x ?? 0, y: p.y ?? 0)
|
|
292
|
+
guard let ev = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: loc, mouseButton: .left)
|
|
293
|
+
else { return "{\"error\":\"fail\"}" }
|
|
294
|
+
postHidTap(ev); return "{\"ok\":true,\"method\":\"hid-tap\"}"
|
|
295
|
+
}
|
|
296
|
+
let loc = CGPoint(x: p.x ?? 0, y: p.y ?? 0)
|
|
297
|
+
let winNum = p.windowNumber ?? 0
|
|
298
|
+
if let ev = makeMouseEvent(.mouseMoved, at: loc, button: .left, windowNumber: winNum) {
|
|
299
|
+
postPerPid(ev, pid: pid, at: loc, windowNumber: winNum, button: .left)
|
|
300
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
301
|
+
}
|
|
302
|
+
return "{\"error\":\"fail\"}"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
func doDrag(_ p: Input) -> String {
|
|
306
|
+
guard let pid = p.pid, pid > 0 else { return "{\"error\":\"no pid\"}" }
|
|
307
|
+
let from = CGPoint(x: p.fromX ?? 0, y: p.fromY ?? 0)
|
|
308
|
+
let to = CGPoint(x: p.toX ?? 0, y: p.toY ?? 0)
|
|
309
|
+
let ms = p.durationMs ?? 300; let b = btn(p.button)
|
|
310
|
+
let winNum = p.windowNumber ?? 0
|
|
311
|
+
let steps = max(2, min(60, Int(ceil(Double(ms) / 16.0))))
|
|
312
|
+
let delay = max(0, (ms * 1000) / steps)
|
|
313
|
+
if !skylightMouseAvailable || isFrontmost(pid) {
|
|
314
|
+
guard let dn = CGEvent(mouseEventSource: nil, mouseType: downT(b), mouseCursorPosition: from, mouseButton: b)
|
|
315
|
+
else { return "{\"error\":\"fail\"}" }
|
|
316
|
+
postHidTap(dn)
|
|
317
|
+
for n in 1...steps {
|
|
318
|
+
let t = Double(n) / Double(steps)
|
|
319
|
+
let pt = CGPoint(x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t)
|
|
320
|
+
if let ev = CGEvent(mouseEventSource: nil, mouseType: dragT(b), mouseCursorPosition: pt, mouseButton: b) { postHidTap(ev) }
|
|
321
|
+
if delay > 0 && n < steps { usleep(UInt32(delay)) }
|
|
322
|
+
}
|
|
323
|
+
if let up = CGEvent(mouseEventSource: nil, mouseType: upT(b), mouseCursorPosition: to, mouseButton: b) { postHidTap(up) }
|
|
324
|
+
return "{\"ok\":true,\"method\":\"hid-tap\"}"
|
|
325
|
+
}
|
|
326
|
+
focusWithoutRaise(pid: pid, windowID: winNum)
|
|
327
|
+
guard let dn = makeMouseEvent(downT(b), at: from, button: b, windowNumber: winNum)
|
|
328
|
+
else { return "{\"error\":\"fail\"}" }
|
|
329
|
+
postPerPid(dn, pid: pid, at: from, windowNumber: winNum, button: b)
|
|
330
|
+
for n in 1...steps {
|
|
331
|
+
let t = Double(n) / Double(steps)
|
|
332
|
+
let pt = CGPoint(x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t)
|
|
333
|
+
if let ev = makeMouseEvent(dragT(b), at: pt, button: b, windowNumber: winNum) {
|
|
334
|
+
postPerPid(ev, pid: pid, at: pt, windowNumber: winNum, button: b)
|
|
335
|
+
}
|
|
336
|
+
if delay > 0 && n < steps { usleep(UInt32(delay)) }
|
|
337
|
+
}
|
|
338
|
+
if let up = makeMouseEvent(upT(b), at: to, button: b, windowNumber: winNum) {
|
|
339
|
+
postPerPid(up, pid: pid, at: to, windowNumber: winNum, button: b)
|
|
340
|
+
}
|
|
341
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func doScroll(_ p: Input) -> String {
|
|
345
|
+
// Scroll events use CGEventCreateScrollWheelEvent; per-pid scroll posting is
|
|
346
|
+
// unreliable across app types, so we route via HID-tap but still report method.
|
|
347
|
+
let dy = Int32(-(p.deltaY ?? 0)); let dx = Int32(p.deltaX ?? 0)
|
|
348
|
+
guard let ev = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: dy, wheel2: dx, wheel3: 0)
|
|
349
|
+
else { return "{\"error\":\"fail\"}" }
|
|
350
|
+
// Best-effort per-pid if we have a pid; scroll doesn't move the cursor noticeably anyway.
|
|
351
|
+
if let pid = p.pid, pid > 0, slPostToPid != nil, !isFrontmost(pid) {
|
|
352
|
+
slPostToPid?(pid, ev)
|
|
353
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
354
|
+
}
|
|
355
|
+
postHidTap(ev)
|
|
356
|
+
return "{\"ok\":true,\"method\":\"hid-tap\"}"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
func doPressKey(_ p: Input) -> String {
|
|
360
|
+
guard let pid = p.pid, pid > 0 else { return "{\"error\":\"no pid\"}" }
|
|
361
|
+
let code = UInt16(p.keyCode ?? 0); let flags = CGEventFlags(rawValue: UInt64(p.flags ?? 0))
|
|
362
|
+
guard let dn = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: true),
|
|
363
|
+
let up = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: false)
|
|
364
|
+
else { return "{\"error\":\"fail\"}" }
|
|
365
|
+
dn.flags = flags; up.flags = flags
|
|
366
|
+
// Keyboard: public CGEventPostToPid is sufficient (no SkyLight needed).
|
|
367
|
+
dn.postToPid(pid); up.postToPid(pid)
|
|
368
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func doTypeBatch(_ p: Input) -> String {
|
|
372
|
+
guard let pid = p.pid, pid > 0 else { return "{\"error\":\"no pid\"}" }
|
|
373
|
+
guard let keys = p.keys else { return "{\"error\":\"missing keys\"}" }
|
|
374
|
+
let SHIFT = CGEventFlags(rawValue: 0x00020000)
|
|
375
|
+
for entry in keys {
|
|
376
|
+
let code = UInt16(entry.code); let flags: CGEventFlags = (entry.shift ?? false) ? SHIFT : []
|
|
377
|
+
guard let dn = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: true),
|
|
378
|
+
let up = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: false) else { continue }
|
|
379
|
+
dn.flags = flags; up.flags = flags
|
|
380
|
+
dn.postToPid(pid); up.postToPid(pid)
|
|
381
|
+
}
|
|
382
|
+
return "{\"ok\":true,\"method\":\"per-pid\"}"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// MARK: - Dispatch
|
|
386
|
+
|
|
387
|
+
guard let line = readLine(), let data = line.data(using: .utf8),
|
|
388
|
+
let input = try? JSONDecoder().decode(Input.self, from: data)
|
|
389
|
+
else { out("{\"error\":\"invalid JSON\"}"); exit(1) }
|
|
390
|
+
|
|
391
|
+
switch input.command {
|
|
392
|
+
case "click": out(doClick(input))
|
|
393
|
+
case "doubleClick": out(doDoubleClick(input))
|
|
394
|
+
case "move": out(doMove(input))
|
|
395
|
+
case "drag": out(doDrag(input))
|
|
396
|
+
case "scroll": out(doScroll(input))
|
|
397
|
+
case "pressKey": out(doPressKey(input))
|
|
398
|
+
case "typeBatch": out(doTypeBatch(input))
|
|
399
|
+
case "keepAlive": out(doKeepAlive(input))
|
|
400
|
+
case "ping": out(skylightMouseAvailable ? "{\"ok\":true,\"skylight\":true}" : "{\"ok\":true,\"skylight\":false}")
|
|
401
|
+
default: out("{\"error\":\"unknown\"}")
|
|
402
|
+
}
|