linkshell-cli 0.7.0 → 0.8.0
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/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/input-helper.d.ts +1 -0
- package/dist/host/src/input-helper.js +208 -0
- package/dist/host/src/input-helper.js.map +1 -0
- package/dist/host/src/input.d.ts +121 -0
- package/dist/host/src/input.js +159 -0
- package/dist/host/src/input.js.map +1 -0
- package/dist/host/src/screen-viewer.d.ts +1 -0
- package/dist/host/src/screen-viewer.js +783 -0
- package/dist/host/src/screen-viewer.js.map +1 -0
- package/dist/host/src/screen.d.ts +2 -0
- package/dist/host/src/screen.js +43 -58
- package/dist/host/src/screen.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const INPUT_HELPER_SOURCE: string;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// The source of the helper that moves this Mac's pointer and presses its keys
|
|
2
|
+
// (see `input.ts`). It is kept here as text and compiled on the computer the
|
|
3
|
+
// first time someone asks to control the screen: a Swift file needs no step in
|
|
4
|
+
// the package's build and no binary in it, and the compiler comes with the
|
|
5
|
+
// command line tools a machine running coding agents already has.
|
|
6
|
+
export const INPUT_HELPER_SOURCE = String.raw `import ApplicationServices
|
|
7
|
+
import CoreGraphics
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
// LinkShell's hands on this Mac: reads one JSON command a line on stdin and
|
|
11
|
+
// posts the mouse or keyboard event it describes. Positions are fractions of
|
|
12
|
+
// the display being watched. With --dry-run the events are printed, not posted.
|
|
13
|
+
|
|
14
|
+
let arguments = CommandLine.arguments
|
|
15
|
+
let dryRun = arguments.contains("--dry-run")
|
|
16
|
+
let screenNumber = arguments.dropFirst().compactMap { Int($0) }.first ?? 0
|
|
17
|
+
|
|
18
|
+
func emit(_ object: [String: Any]) {
|
|
19
|
+
guard let data = try? JSONSerialization.data(withJSONObject: object), let line = String(data: data, encoding: .utf8) else { return }
|
|
20
|
+
FileHandle.standardOutput.write((line + "\n").data(using: .utf8)!)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func displayBounds() -> CGRect {
|
|
24
|
+
var count: UInt32 = 0
|
|
25
|
+
CGGetActiveDisplayList(0, nil, &count)
|
|
26
|
+
var ids = [CGDirectDisplayID](repeating: 0, count: Int(count))
|
|
27
|
+
CGGetActiveDisplayList(count, &ids, &count)
|
|
28
|
+
let id = screenNumber < Int(count) ? ids[screenNumber] : CGMainDisplayID()
|
|
29
|
+
return CGDisplayBounds(id)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func trusted() -> Bool { dryRun || AXIsProcessTrusted() }
|
|
33
|
+
|
|
34
|
+
let source = CGEventSource(stateID: .hidSystemState)
|
|
35
|
+
// Events from here must not hold back the real mouse and keyboard.
|
|
36
|
+
source?.localEventsSuppressionInterval = 0
|
|
37
|
+
|
|
38
|
+
var bounds = displayBounds()
|
|
39
|
+
var position = CGEvent(source: nil)?.location ?? CGPoint(x: bounds.midX, y: bounds.midY)
|
|
40
|
+
var held: [String: Bool] = [:]
|
|
41
|
+
var lastMove = Date.distantPast
|
|
42
|
+
var lastReported = CGPoint(x: -1, y: -1)
|
|
43
|
+
var wasTrusted = trusted()
|
|
44
|
+
|
|
45
|
+
let keyCodes: [String: CGKeyCode] = [
|
|
46
|
+
"a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7, "c": 8, "v": 9, "b": 11, "q": 12, "w": 13, "e": 14, "r": 15,
|
|
47
|
+
"y": 16, "t": 17, "1": 18, "2": 19, "3": 20, "4": 21, "6": 22, "5": 23, "=": 24, "9": 25, "7": 26, "-": 27, "8": 28, "0": 29,
|
|
48
|
+
"]": 30, "o": 31, "u": 32, "[": 33, "i": 34, "p": 35, "return": 36, "l": 37, "j": 38, "'": 39, "k": 40, ";": 41, "\\": 42,
|
|
49
|
+
",": 43, "/": 44, "n": 45, "m": 46, ".": 47, "tab": 48, "space": 49, "\u{60}": 50, "backspace": 51, "escape": 53,
|
|
50
|
+
"left": 123, "right": 124, "down": 125, "up": 126, "delete": 117, "home": 115, "end": 119, "pageup": 116, "pagedown": 121,
|
|
51
|
+
"f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97, "f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
func flags(_ names: Any?) -> CGEventFlags {
|
|
55
|
+
var result: CGEventFlags = []
|
|
56
|
+
for name in (names as? [String]) ?? [] {
|
|
57
|
+
switch name {
|
|
58
|
+
case "cmd": result.insert(.maskCommand)
|
|
59
|
+
case "shift": result.insert(.maskShift)
|
|
60
|
+
case "alt": result.insert(.maskAlternate)
|
|
61
|
+
case "ctrl": result.insert(.maskControl)
|
|
62
|
+
default: break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func post(_ event: CGEvent?, _ what: [String: Any]) {
|
|
69
|
+
if dryRun {
|
|
70
|
+
emit(what.merging(["t": "posted"]) { current, _ in current })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
event?.post(tap: .cghidEventTap)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func point(_ command: [String: Any]) -> CGPoint? {
|
|
77
|
+
guard let x = command["x"] as? Double, let y = command["y"] as? Double else { return nil }
|
|
78
|
+
// Just inside the far edges: a point on the edge itself belongs to the next display.
|
|
79
|
+
return CGPoint(
|
|
80
|
+
x: bounds.minX + min(max(x, 0), 1) * (bounds.width - 1),
|
|
81
|
+
y: bounds.minY + min(max(y, 0), 1) * (bounds.height - 1)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func move(to target: CGPoint) {
|
|
86
|
+
let kind: CGEventType = held["left"] == true ? .leftMouseDragged : held["right"] == true ? .rightMouseDragged : .mouseMoved
|
|
87
|
+
let event = CGEvent(mouseEventSource: source, mouseType: kind, mouseCursorPosition: target, mouseButton: held["right"] == true ? .right : .left)
|
|
88
|
+
event?.setIntegerValueField(.mouseEventDeltaX, value: Int64(target.x - position.x))
|
|
89
|
+
event?.setIntegerValueField(.mouseEventDeltaY, value: Int64(target.y - position.y))
|
|
90
|
+
event?.flags = []
|
|
91
|
+
position = target
|
|
92
|
+
lastMove = Date()
|
|
93
|
+
post(event, ["kind": kind == .mouseMoved ? "move" : "drag", "x": Double(target.x), "y": Double(target.y)])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func button(_ name: String, down: Bool, clicks: Int64, modifiers: CGEventFlags) {
|
|
97
|
+
if held[name] == down { return }
|
|
98
|
+
held[name] = down
|
|
99
|
+
let right = name == "right"
|
|
100
|
+
let kind: CGEventType = right ? (down ? .rightMouseDown : .rightMouseUp) : (down ? .leftMouseDown : .leftMouseUp)
|
|
101
|
+
let event = CGEvent(mouseEventSource: source, mouseType: kind, mouseCursorPosition: position, mouseButton: right ? .right : .left)
|
|
102
|
+
event?.setIntegerValueField(.mouseEventClickState, value: clicks)
|
|
103
|
+
event?.flags = modifiers
|
|
104
|
+
lastMove = Date()
|
|
105
|
+
post(event, ["kind": down ? "down" : "up", "b": name, "n": clicks, "x": Double(position.x), "y": Double(position.y), "flags": modifiers.rawValue])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func key(_ code: CGKeyCode, modifiers: CGEventFlags, name: String) {
|
|
109
|
+
for down in [true, false] {
|
|
110
|
+
let event = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: down)
|
|
111
|
+
event?.flags = modifiers
|
|
112
|
+
post(event, ["kind": down ? "keydown" : "keyup", "k": name, "code": Int(code), "flags": modifiers.rawValue])
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func type(_ text: String) {
|
|
117
|
+
// A string a key event: whatever the keyboard layout or input method, the characters arrive as written.
|
|
118
|
+
let units = Array(text.utf16)
|
|
119
|
+
var index = 0
|
|
120
|
+
while index < units.count {
|
|
121
|
+
var end = min(index + 20, units.count)
|
|
122
|
+
// Not between the halves of a surrogate pair.
|
|
123
|
+
if end < units.count, UTF16.isLeadSurrogate(units[end - 1]) { end -= 1 }
|
|
124
|
+
let chunk = Array(units[index..<end])
|
|
125
|
+
for down in [true, false] {
|
|
126
|
+
let event = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: down)
|
|
127
|
+
event?.flags = []
|
|
128
|
+
event?.keyboardSetUnicodeString(stringLength: chunk.count, unicodeString: chunk)
|
|
129
|
+
post(event, ["kind": down ? "textdown" : "textup", "s": String(utf16CodeUnits: chunk, count: chunk.count)])
|
|
130
|
+
}
|
|
131
|
+
index = end
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func releaseAll() {
|
|
136
|
+
for (name, down) in held where down { button(name, down: false, clicks: 1, modifiers: []) }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func handle(_ command: [String: Any]) {
|
|
140
|
+
guard let kind = command["t"] as? String else { return }
|
|
141
|
+
if kind == "prompt" {
|
|
142
|
+
// Shows the system's "wants to control this computer" dialog on the Mac, once.
|
|
143
|
+
if !dryRun {
|
|
144
|
+
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
|
|
145
|
+
_ = AXIsProcessTrustedWithOptions(options)
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
guard trusted() else { return }
|
|
150
|
+
switch kind {
|
|
151
|
+
case "move":
|
|
152
|
+
if let target = point(command) { move(to: target) }
|
|
153
|
+
case "down", "up":
|
|
154
|
+
let name = (command["b"] as? String) == "right" ? "right" : "left"
|
|
155
|
+
let clicks = Int64(min(max((command["n"] as? Int) ?? 1, 1), 3))
|
|
156
|
+
button(name, down: kind == "down", clicks: clicks, modifiers: flags(command["m"]))
|
|
157
|
+
case "scroll":
|
|
158
|
+
let dx = Int32(max(min((command["dx"] as? Double) ?? 0, 2000), -2000))
|
|
159
|
+
let dy = Int32(max(min((command["dy"] as? Double) ?? 0, 2000), -2000))
|
|
160
|
+
let event = CGEvent(scrollWheelEvent2Source: source, units: .pixel, wheelCount: 2, wheel1: dy, wheel2: dx, wheel3: 0)
|
|
161
|
+
event?.flags = flags(command["m"])
|
|
162
|
+
post(event, ["kind": "scroll", "dx": Int(dx), "dy": Int(dy)])
|
|
163
|
+
case "key":
|
|
164
|
+
guard let name = command["k"] as? String, let code = keyCodes[name] else { return }
|
|
165
|
+
key(code, modifiers: flags(command["m"]), name: name)
|
|
166
|
+
case "text":
|
|
167
|
+
if let text = command["s"] as? String { type(text) }
|
|
168
|
+
default:
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func report() {
|
|
174
|
+
let now = trusted()
|
|
175
|
+
if now != wasTrusted {
|
|
176
|
+
wasTrusted = now
|
|
177
|
+
emit(["t": "trusted", "v": now])
|
|
178
|
+
}
|
|
179
|
+
// Where the pointer is when someone at the Mac (or another program) moved it.
|
|
180
|
+
guard Date().timeIntervalSince(lastMove) > 0.3, let location = CGEvent(source: nil)?.location else { return }
|
|
181
|
+
bounds = displayBounds()
|
|
182
|
+
if abs(location.x - lastReported.x) < 0.5, abs(location.y - lastReported.y) < 0.5 { return }
|
|
183
|
+
lastReported = location
|
|
184
|
+
position = location
|
|
185
|
+
guard bounds.contains(location) else { return }
|
|
186
|
+
emit(["t": "cursor", "x": Double((location.x - bounds.minX) / max(bounds.width - 1, 1)), "y": Double((location.y - bounds.minY) / max(bounds.height - 1, 1))])
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
emit(["t": "ready", "trusted": wasTrusted, "w": Double(bounds.width), "h": Double(bounds.height)])
|
|
190
|
+
|
|
191
|
+
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
192
|
+
timer.schedule(deadline: .now(), repeating: .milliseconds(250))
|
|
193
|
+
timer.setEventHandler { report() }
|
|
194
|
+
timer.resume()
|
|
195
|
+
|
|
196
|
+
Thread.detachNewThread {
|
|
197
|
+
while let line = readLine(strippingNewline: true) {
|
|
198
|
+
guard let data = line.data(using: .utf8), let command = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else { continue }
|
|
199
|
+
DispatchQueue.main.sync { handle(command) }
|
|
200
|
+
}
|
|
201
|
+
// The viewer left: nothing stays pressed.
|
|
202
|
+
DispatchQueue.main.sync { releaseAll() }
|
|
203
|
+
exit(0)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
dispatchMain()
|
|
207
|
+
`;
|
|
208
|
+
//# sourceMappingURL=input-helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-helper.js","sourceRoot":"","sources":["../../../../host/src/input-helper.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,+EAA+E;AAC/E,2EAA2E;AAC3E,kEAAkE;AAElE,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyM5C,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** What a viewer may ask the helper to do. Positions are fractions of the display being watched. */
|
|
3
|
+
export declare const inputEvent: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
4
|
+
t: z.ZodLiteral<"move">;
|
|
5
|
+
x: z.ZodNumber;
|
|
6
|
+
y: z.ZodNumber;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
t: "move";
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
}, {
|
|
12
|
+
t: "move";
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}>, z.ZodObject<{
|
|
16
|
+
t: z.ZodLiteral<"down">;
|
|
17
|
+
b: z.ZodEnum<["left", "right"]>;
|
|
18
|
+
n: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
m: z.ZodOptional<z.ZodArray<z.ZodEnum<["cmd", "shift", "alt", "ctrl"]>, "many">>;
|
|
20
|
+
}, "strip", z.ZodTypeAny, {
|
|
21
|
+
t: "down";
|
|
22
|
+
b: "left" | "right";
|
|
23
|
+
n?: number | undefined;
|
|
24
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
t: "down";
|
|
27
|
+
b: "left" | "right";
|
|
28
|
+
n?: number | undefined;
|
|
29
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
30
|
+
}>, z.ZodObject<{
|
|
31
|
+
t: z.ZodLiteral<"up">;
|
|
32
|
+
b: z.ZodEnum<["left", "right"]>;
|
|
33
|
+
n: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
m: z.ZodOptional<z.ZodArray<z.ZodEnum<["cmd", "shift", "alt", "ctrl"]>, "many">>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
t: "up";
|
|
37
|
+
b: "left" | "right";
|
|
38
|
+
n?: number | undefined;
|
|
39
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
t: "up";
|
|
42
|
+
b: "left" | "right";
|
|
43
|
+
n?: number | undefined;
|
|
44
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
45
|
+
}>, z.ZodObject<{
|
|
46
|
+
t: z.ZodLiteral<"scroll">;
|
|
47
|
+
dx: z.ZodNumber;
|
|
48
|
+
dy: z.ZodNumber;
|
|
49
|
+
m: z.ZodOptional<z.ZodArray<z.ZodEnum<["cmd", "shift", "alt", "ctrl"]>, "many">>;
|
|
50
|
+
}, "strip", z.ZodTypeAny, {
|
|
51
|
+
t: "scroll";
|
|
52
|
+
dx: number;
|
|
53
|
+
dy: number;
|
|
54
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
55
|
+
}, {
|
|
56
|
+
t: "scroll";
|
|
57
|
+
dx: number;
|
|
58
|
+
dy: number;
|
|
59
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
60
|
+
}>, z.ZodObject<{
|
|
61
|
+
t: z.ZodLiteral<"key">;
|
|
62
|
+
k: z.ZodString;
|
|
63
|
+
m: z.ZodOptional<z.ZodArray<z.ZodEnum<["cmd", "shift", "alt", "ctrl"]>, "many">>;
|
|
64
|
+
}, "strip", z.ZodTypeAny, {
|
|
65
|
+
k: string;
|
|
66
|
+
t: "key";
|
|
67
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
68
|
+
}, {
|
|
69
|
+
k: string;
|
|
70
|
+
t: "key";
|
|
71
|
+
m?: ("shift" | "cmd" | "alt" | "ctrl")[] | undefined;
|
|
72
|
+
}>, z.ZodObject<{
|
|
73
|
+
t: z.ZodLiteral<"text">;
|
|
74
|
+
s: z.ZodString;
|
|
75
|
+
}, "strip", z.ZodTypeAny, {
|
|
76
|
+
t: "text";
|
|
77
|
+
s: string;
|
|
78
|
+
}, {
|
|
79
|
+
t: "text";
|
|
80
|
+
s: string;
|
|
81
|
+
}>, z.ZodObject<{
|
|
82
|
+
t: z.ZodLiteral<"prompt">;
|
|
83
|
+
}, "strip", z.ZodTypeAny, {
|
|
84
|
+
t: "prompt";
|
|
85
|
+
}, {
|
|
86
|
+
t: "prompt";
|
|
87
|
+
}>]>;
|
|
88
|
+
export type InputEvent = z.infer<typeof inputEvent>;
|
|
89
|
+
export type ControlState = {
|
|
90
|
+
available: true;
|
|
91
|
+
trusted: boolean;
|
|
92
|
+
} | {
|
|
93
|
+
available: false;
|
|
94
|
+
reason: string;
|
|
95
|
+
};
|
|
96
|
+
export interface InputListener {
|
|
97
|
+
state(state: ControlState): void;
|
|
98
|
+
/** Where the pointer is, when something other than this viewer moved it. */
|
|
99
|
+
cursor(x: number, y: number): void;
|
|
100
|
+
/** Dry runs only: the event that would have been posted. */
|
|
101
|
+
posted?(event: Record<string, unknown>): void;
|
|
102
|
+
}
|
|
103
|
+
/** The compiled helper's path; built on first use, once per version of its source. */
|
|
104
|
+
export declare function inputHelper(log?: (message: string) => void): Promise<string>;
|
|
105
|
+
/** One viewer's control of one display. */
|
|
106
|
+
export declare class InputControl {
|
|
107
|
+
private readonly listener;
|
|
108
|
+
private readonly log;
|
|
109
|
+
private readonly options;
|
|
110
|
+
private child?;
|
|
111
|
+
private stopped;
|
|
112
|
+
constructor(listener: InputListener, log: (message: string) => void, options?: {
|
|
113
|
+
dryRun?: boolean;
|
|
114
|
+
});
|
|
115
|
+
/** Starts the helper for the display ffmpeg calls "Capture screen `screen`". Reports through the listener; never throws. */
|
|
116
|
+
start(screen: number): Promise<void>;
|
|
117
|
+
/** Passes on one event from the viewer; anything that is not one is dropped. */
|
|
118
|
+
send(raw: unknown): void;
|
|
119
|
+
/** Ends the helper; on its way out it lets go of any button still held. */
|
|
120
|
+
stop(): void;
|
|
121
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { INPUT_HELPER_SOURCE } from "./input-helper.js";
|
|
11
|
+
// Controlling the computer from the phone: the screen viewer sends pointer and
|
|
12
|
+
// key events down the same socket the picture comes up, and a small helper
|
|
13
|
+
// posts them to the system. macOS only; the helper needs the Accessibility
|
|
14
|
+
// permission, which the system grants to whatever started the host (the
|
|
15
|
+
// terminal, as with Screen Recording).
|
|
16
|
+
const run = promisify(execFile);
|
|
17
|
+
const unit = z.number().min(0).max(1);
|
|
18
|
+
const button = z.enum(["left", "right"]);
|
|
19
|
+
const modifiers = z.array(z.enum(["cmd", "shift", "alt", "ctrl"])).max(4).optional();
|
|
20
|
+
const clicks = z.number().int().min(1).max(3).optional();
|
|
21
|
+
const delta = z.number().finite();
|
|
22
|
+
/** What a viewer may ask the helper to do. Positions are fractions of the display being watched. */
|
|
23
|
+
export const inputEvent = z.discriminatedUnion("t", [
|
|
24
|
+
z.object({ t: z.literal("move"), x: unit, y: unit }),
|
|
25
|
+
z.object({ t: z.literal("down"), b: button, n: clicks, m: modifiers }),
|
|
26
|
+
z.object({ t: z.literal("up"), b: button, n: clicks, m: modifiers }),
|
|
27
|
+
z.object({ t: z.literal("scroll"), dx: delta, dy: delta, m: modifiers }),
|
|
28
|
+
z.object({ t: z.literal("key"), k: z.string().min(1).max(16), m: modifiers }),
|
|
29
|
+
z.object({ t: z.literal("text"), s: z.string().min(1).max(4000) }),
|
|
30
|
+
/** Have the system ask, on the computer, for the permission to control it. */
|
|
31
|
+
z.object({ t: z.literal("prompt") }),
|
|
32
|
+
]);
|
|
33
|
+
function binDir() {
|
|
34
|
+
return join(process.env.LINKSHELL_HOME || join(homedir(), ".linkshell"), "bin");
|
|
35
|
+
}
|
|
36
|
+
async function build(log) {
|
|
37
|
+
const hash = createHash("sha256").update(INPUT_HELPER_SOURCE).digest("hex").slice(0, 12);
|
|
38
|
+
const dir = binDir();
|
|
39
|
+
const path = join(dir, `input-${hash}`);
|
|
40
|
+
if (existsSync(path))
|
|
41
|
+
return path;
|
|
42
|
+
// Asking the `swiftc` shim on a Mac without the tools opens an install dialog there: look first.
|
|
43
|
+
const tools = await run("/usr/bin/xcode-select", ["-p"], { timeout: 5000 }).then(() => true, () => false);
|
|
44
|
+
if (!tools)
|
|
45
|
+
throw new Error("控制电脑需要 Xcode 命令行工具:在电脑上运行 xcode-select --install 后再试");
|
|
46
|
+
await mkdir(dir, { recursive: true });
|
|
47
|
+
const source = `${path}.swift`;
|
|
48
|
+
const partial = `${path}.${process.pid}.partial`;
|
|
49
|
+
await writeFile(source, INPUT_HELPER_SOURCE);
|
|
50
|
+
try {
|
|
51
|
+
await run("/usr/bin/swiftc", ["-O", "-swift-version", "5", "-o", partial, source], { timeout: 180_000 });
|
|
52
|
+
await rename(partial, path);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
log(`[screen] the input helper did not compile: ${error.stderr?.trim() || error.message}`);
|
|
56
|
+
throw new Error("控制组件没能在这台电脑上编译,详情见电脑上的 LinkShell 日志");
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await rm(source, { force: true });
|
|
60
|
+
await rm(partial, { force: true });
|
|
61
|
+
}
|
|
62
|
+
// Helpers built from an earlier version's source.
|
|
63
|
+
for (const name of await readdir(dir).catch(() => [])) {
|
|
64
|
+
if (/^input-[0-9a-f]{12}$/.test(name) && name !== `input-${hash}`)
|
|
65
|
+
await rm(join(dir, name), { force: true });
|
|
66
|
+
}
|
|
67
|
+
log("[screen] built the input helper");
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
let helper;
|
|
71
|
+
/** The compiled helper's path; built on first use, once per version of its source. */
|
|
72
|
+
export function inputHelper(log = () => { }) {
|
|
73
|
+
helper ??= build(log).catch((error) => {
|
|
74
|
+
helper = undefined;
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
return helper;
|
|
78
|
+
}
|
|
79
|
+
/** One viewer's control of one display. */
|
|
80
|
+
export class InputControl {
|
|
81
|
+
listener;
|
|
82
|
+
log;
|
|
83
|
+
options;
|
|
84
|
+
child;
|
|
85
|
+
stopped = false;
|
|
86
|
+
constructor(listener, log, options = {}) {
|
|
87
|
+
this.listener = listener;
|
|
88
|
+
this.log = log;
|
|
89
|
+
this.options = options;
|
|
90
|
+
}
|
|
91
|
+
/** Starts the helper for the display ffmpeg calls "Capture screen `screen`". Reports through the listener; never throws. */
|
|
92
|
+
async start(screen) {
|
|
93
|
+
if (this.child || this.stopped)
|
|
94
|
+
return;
|
|
95
|
+
if (process.platform !== "darwin") {
|
|
96
|
+
this.listener.state({ available: false, reason: "暂时只能控制 macOS 电脑" });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let path;
|
|
100
|
+
try {
|
|
101
|
+
path = await inputHelper(this.log);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.listener.state({ available: false, reason: error.message });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (this.child || this.stopped)
|
|
108
|
+
return;
|
|
109
|
+
const dryRun = this.options.dryRun ?? process.env.LINKSHELL_INPUT_DRY_RUN === "1";
|
|
110
|
+
const child = spawn(path, [String(screen), ...(dryRun ? ["--dry-run"] : [])], { stdio: ["pipe", "pipe", "ignore"] });
|
|
111
|
+
this.child = child;
|
|
112
|
+
child.on("error", (error) => {
|
|
113
|
+
this.log(`[screen] the input helper did not start: ${error.message}`);
|
|
114
|
+
this.listener.state({ available: false, reason: "控制组件没能启动" });
|
|
115
|
+
});
|
|
116
|
+
child.on("exit", () => {
|
|
117
|
+
if (this.child === child)
|
|
118
|
+
this.child = undefined;
|
|
119
|
+
});
|
|
120
|
+
// The viewer going away mid-write is not an error worth a crash.
|
|
121
|
+
child.stdin.on("error", () => { });
|
|
122
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
123
|
+
let message;
|
|
124
|
+
try {
|
|
125
|
+
message = JSON.parse(line);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (message.t === "ready")
|
|
131
|
+
this.listener.state({ available: true, trusted: message.trusted === true });
|
|
132
|
+
else if (message.t === "trusted")
|
|
133
|
+
this.listener.state({ available: true, trusted: message.v === true });
|
|
134
|
+
else if (message.t === "cursor" && typeof message.x === "number" && typeof message.y === "number")
|
|
135
|
+
this.listener.cursor(message.x, message.y);
|
|
136
|
+
else if (message.t === "posted")
|
|
137
|
+
this.listener.posted?.(message);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Passes on one event from the viewer; anything that is not one is dropped. */
|
|
141
|
+
send(raw) {
|
|
142
|
+
const stdin = this.child?.stdin;
|
|
143
|
+
if (!stdin?.writable)
|
|
144
|
+
return;
|
|
145
|
+
const parsed = inputEvent.safeParse(raw);
|
|
146
|
+
if (parsed.success)
|
|
147
|
+
stdin.write(`${JSON.stringify(parsed.data)}\n`);
|
|
148
|
+
}
|
|
149
|
+
/** Ends the helper; on its way out it lets go of any button still held. */
|
|
150
|
+
stop() {
|
|
151
|
+
this.stopped = true;
|
|
152
|
+
this.child?.stdin?.end();
|
|
153
|
+
const child = this.child;
|
|
154
|
+
if (child)
|
|
155
|
+
setTimeout(() => child.exitCode === null && child.kill("SIGKILL"), 1000).unref();
|
|
156
|
+
this.child = undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../../../host/src/input.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function viewerPage(): string;
|