linkshell-cli 0.9.0 → 0.10.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/README.md +11 -6
- package/dist/cli/src/commands/gateway.d.ts +2 -0
- package/dist/cli/src/commands/gateway.js +29 -0
- package/dist/cli/src/commands/gateway.js.map +1 -0
- package/dist/cli/src/commands/host.d.ts +2 -0
- package/dist/cli/src/commands/host.js +12 -2
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/src/commands/setup.js +4 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/index.js +12 -334
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +0 -7
- package/dist/cli/src/utils/daemon.js +1 -18
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/gateway/src/accounts.d.ts +6 -0
- package/dist/gateway/src/accounts.js +32 -0
- package/dist/gateway/src/accounts.js.map +1 -0
- package/dist/gateway/src/index.d.ts +4 -0
- package/dist/gateway/src/index.js +5 -0
- package/dist/gateway/src/index.js.map +1 -0
- package/dist/gateway/src/main.d.ts +1 -0
- package/dist/gateway/src/main.js +57 -0
- package/dist/gateway/src/main.js.map +1 -0
- package/dist/gateway/src/rate-limit.d.ts +22 -0
- package/dist/gateway/src/rate-limit.js +66 -0
- package/dist/gateway/src/rate-limit.js.map +1 -0
- package/dist/gateway/src/relay.d.ts +84 -0
- package/dist/gateway/src/relay.js +434 -0
- package/dist/gateway/src/relay.js.map +1 -0
- package/dist/gateway/src/serve.d.ts +34 -0
- package/dist/gateway/src/serve.js +102 -0
- package/dist/gateway/src/serve.js.map +1 -0
- package/dist/gateway/src/store.d.ts +35 -0
- package/dist/gateway/src/store.js +109 -0
- package/dist/gateway/src/store.js.map +1 -0
- package/dist/gateway/src/subscription.d.ts +18 -0
- package/dist/gateway/src/subscription.js +46 -0
- package/dist/gateway/src/subscription.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
- package/dist/host/src/fs.d.ts +3 -3
- package/dist/host/src/input.d.ts +4 -4
- package/dist/host/src/terminals.d.ts +1 -1
- package/dist/wire/src/model.d.ts +6 -6
- package/dist/wire/src/relay.d.ts +8 -8
- package/dist/wire/src/rpc.d.ts +818 -818
- package/dist/wire/src/updates.d.ts +18 -18
- package/package.json +4 -13
- package/src/commands/gateway.ts +31 -0
- package/src/commands/host.ts +13 -2
- package/src/commands/setup.ts +4 -2
- package/src/index.ts +14 -429
- package/src/utils/daemon.ts +2 -27
- package/dist/cli/src/config.d.ts +0 -14
- package/dist/cli/src/config.js +0 -23
- package/dist/cli/src/config.js.map +0 -1
- package/dist/cli/src/machine-id.d.ts +0 -6
- package/dist/cli/src/machine-id.js +0 -50
- package/dist/cli/src/machine-id.js.map +0 -1
- package/dist/cli/src/providers.d.ts +0 -13
- package/dist/cli/src/providers.js +0 -77
- package/dist/cli/src/providers.js.map +0 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
- package/dist/cli/src/runtime/acp/acp-client.js +0 -430
- package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
- package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
- package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
- package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
- package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
- package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
- package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
- package/dist/cli/src/runtime/bridge-session.js +0 -1364
- package/dist/cli/src/runtime/bridge-session.js.map +0 -1
- package/dist/cli/src/runtime/hook-command.d.ts +0 -9
- package/dist/cli/src/runtime/hook-command.js +0 -80
- package/dist/cli/src/runtime/hook-command.js.map +0 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
- package/dist/cli/src/runtime/screen-fallback.js +0 -202
- package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
- package/dist/cli/src/runtime/screen-share.d.ts +0 -34
- package/dist/cli/src/runtime/screen-share.js +0 -376
- package/dist/cli/src/runtime/screen-share.js.map +0 -1
- package/dist/cli/src/runtime/scrollback.d.ts +0 -11
- package/dist/cli/src/runtime/scrollback.js +0 -33
- package/dist/cli/src/runtime/scrollback.js.map +0 -1
- package/dist/cli/src/runtime/usage-report.d.ts +0 -34
- package/dist/cli/src/runtime/usage-report.js +0 -332
- package/dist/cli/src/runtime/usage-report.js.map +0 -1
- package/dist/cli/src/utils/keep-awake.d.ts +0 -6
- package/dist/cli/src/utils/keep-awake.js +0 -48
- package/dist/cli/src/utils/keep-awake.js.map +0 -1
- package/dist/cli/src/utils/lan-ip.d.ts +0 -9
- package/dist/cli/src/utils/lan-ip.js +0 -61
- package/dist/cli/src/utils/lan-ip.js.map +0 -1
- package/dist/host/src/input-helper.d.ts +0 -1
- package/dist/host/src/input-helper.js +0 -564
- package/dist/host/src/input-helper.js.map +0 -1
- package/dist/shared-protocol/src/index.d.ts +0 -17029
- package/dist/shared-protocol/src/index.js +0 -1064
- package/dist/shared-protocol/src/index.js.map +0 -1
- package/scripts/copy-web.mjs +0 -34
- package/src/config.ts +0 -36
- package/src/machine-id.ts +0 -61
- package/src/providers.ts +0 -97
- package/src/runtime/acp/acp-client.ts +0 -521
- package/src/runtime/acp/agent-process-discovery.ts +0 -308
- package/src/runtime/acp/agent-session.ts +0 -1186
- package/src/runtime/acp/agent-workspace.ts +0 -6590
- package/src/runtime/acp/claude-sdk-client.ts +0 -1221
- package/src/runtime/acp/claude-stream-json-client.ts +0 -633
- package/src/runtime/acp/json-rpc.ts +0 -184
- package/src/runtime/acp/provider-resolver.ts +0 -154
- package/src/runtime/bridge-session.ts +0 -1549
- package/src/runtime/hook-command.ts +0 -73
- package/src/runtime/screen-fallback.ts +0 -235
- package/src/runtime/screen-share.ts +0 -426
- package/src/runtime/scrollback.ts +0 -43
- package/src/runtime/usage-report.ts +0 -342
- package/src/utils/keep-awake.ts +0 -61
- package/src/utils/lan-ip.ts +0 -61
- package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
- package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
- package/web/assets/TerminalPanel-CTSklkct.js +0 -1
- package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
- package/web/assets/index-BsnDaeCt.css +0 -33
- package/web/assets/index-q85psYZ5.js +0 -36
- package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
- package/web/assets/vendor-react-M7xG3qiw.js +0 -49
- package/web/assets/vendor-xterm-D15THcI4.js +0 -9
- package/web/index.html +0 -37
- package/web/logos/claude.png +0 -0
- package/web/logos/codex.png +0 -0
- package/web/logos/copilot.png +0 -0
- package/web/logos/gemini.png +0 -0
- package/web/logos/linkshell-1024.png +0 -0
- package/web/logos/linkshell-splash.png +0 -0
- package/web/logos/linkshell.png +0 -0
|
@@ -1,564 +0,0 @@
|
|
|
1
|
-
// The source of the helper that moves this Mac's pointer and presses its keys
|
|
2
|
-
// (see `input.ts`). One source, used two ways:
|
|
3
|
-
//
|
|
4
|
-
// - built and signed into `LinkShell.app` when the package is made (see
|
|
5
|
-
// `scripts/build-helper.mjs`). The host has the system open it, so it is an
|
|
6
|
-
// app of its own: the Accessibility permission is asked for, listed and
|
|
7
|
-
// kept under the name LinkShell, whatever terminal started the host.
|
|
8
|
-
// - compiled on the computer the first time it is needed, where a build has no
|
|
9
|
-
// signed app (a checkout without the signing identity). It then runs as the
|
|
10
|
-
// host's child, and the permission is the terminal's.
|
|
11
|
-
export const INPUT_HELPER_SOURCE = String.raw `import AppKit
|
|
12
|
-
import ApplicationServices
|
|
13
|
-
import Carbon
|
|
14
|
-
import CoreGraphics
|
|
15
|
-
import Darwin
|
|
16
|
-
import Foundation
|
|
17
|
-
|
|
18
|
-
// LinkShell's hands on this Mac: reads one JSON command a line and posts the
|
|
19
|
-
// mouse or keyboard event it describes. Positions are fractions of the display
|
|
20
|
-
// a viewer is watching. With --dry-run the events are reported, not posted.
|
|
21
|
-
//
|
|
22
|
-
// <screen> one viewer, commands on stdin, answers on stdout
|
|
23
|
-
// --connect <socket> an app of its own: any number of viewers ("v") over the host's socket,
|
|
24
|
-
// and the screen captures, which it starts so that they are its own too
|
|
25
|
-
// --status what this process is allowed to do, then exit
|
|
26
|
-
// --ask-control | --ask-recording have the system ask for a permission, then exit
|
|
27
|
-
|
|
28
|
-
let arguments = Array(CommandLine.arguments.dropFirst())
|
|
29
|
-
let dryRun = arguments.contains("--dry-run")
|
|
30
|
-
|
|
31
|
-
func argument(after flag: String) -> String? {
|
|
32
|
-
guard let index = arguments.firstIndex(of: flag), index + 1 < arguments.count else { return nil }
|
|
33
|
-
return arguments[index + 1]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
var output = FileHandle.standardOutput
|
|
37
|
-
|
|
38
|
-
func emit(_ object: [String: Any]) {
|
|
39
|
-
guard let data = try? JSONSerialization.data(withJSONObject: object), let line = String(data: data, encoding: .utf8) else { return }
|
|
40
|
-
output.write((line + "\n").data(using: .utf8)!)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/// The app the system holds responsible for this process: the one its privacy settings list.
|
|
44
|
-
func responsibleApp() -> String {
|
|
45
|
-
typealias Lookup = @convention(c) (pid_t) -> pid_t
|
|
46
|
-
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "responsibility_get_pid_responsible_for_pid") else { return "" }
|
|
47
|
-
let pid = unsafeBitCast(symbol, to: Lookup.self)(getpid())
|
|
48
|
-
guard pid > 0 else { return "" }
|
|
49
|
-
var buffer = [CChar](repeating: 0, count: 4096)
|
|
50
|
-
guard proc_pidpath(pid, &buffer, UInt32(buffer.count)) > 0 else { return "" }
|
|
51
|
-
let path = String(cString: buffer)
|
|
52
|
-
// An app is named by its bundle, anything else by its file.
|
|
53
|
-
if let bundle = path.components(separatedBy: "/").last(where: { $0.hasSuffix(".app") }) { return String(bundle.dropLast(4)) }
|
|
54
|
-
return (path as NSString).lastPathComponent
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
func trusted() -> Bool { dryRun || AXIsProcessTrusted() }
|
|
58
|
-
|
|
59
|
-
/// Whether the screen may be recorded. A process that was refused keeps hearing no after the
|
|
60
|
-
/// switch is turned on, so once it has been refused it asks a fresh copy of itself.
|
|
61
|
-
func recording() -> Bool {
|
|
62
|
-
if CGPreflightScreenCaptureAccess() { return true }
|
|
63
|
-
if arguments.contains("--status") { return false }
|
|
64
|
-
guard let program = Bundle.main.executablePath else { return false }
|
|
65
|
-
let check = Process()
|
|
66
|
-
check.executableURL = URL(fileURLWithPath: program)
|
|
67
|
-
check.arguments = ["--status"]
|
|
68
|
-
let answer = Pipe()
|
|
69
|
-
check.standardOutput = answer
|
|
70
|
-
check.standardError = FileHandle.nullDevice
|
|
71
|
-
guard (try? check.run()) != nil else { return false }
|
|
72
|
-
let data = answer.fileHandleForReading.readDataToEndOfFile()
|
|
73
|
-
check.waitUntilExit()
|
|
74
|
-
return ((try? JSONSerialization.jsonObject(with: data)) as? [String: Any])?["recording"] as? Bool ?? false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
func status() -> [String: Any] {
|
|
78
|
-
["t": "status", "trusted": trusted(), "recording": recording(), "app": responsibleApp()]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// The system's own dialog, and the page of its settings where the switch is.
|
|
82
|
-
func askControl() {
|
|
83
|
-
if dryRun || AXIsProcessTrusted() { return }
|
|
84
|
-
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
|
|
85
|
-
_ = AXIsProcessTrustedWithOptions(options)
|
|
86
|
-
if let page = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { NSWorkspace.shared.open(page) }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func askRecording() {
|
|
90
|
-
if CGPreflightScreenCaptureAccess() { return }
|
|
91
|
-
_ = CGRequestScreenCaptureAccess()
|
|
92
|
-
if let page = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") { NSWorkspace.shared.open(page) }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let source = CGEventSource(stateID: .hidSystemState)
|
|
96
|
-
// Events from here must not hold back the real mouse and keyboard.
|
|
97
|
-
source?.localEventsSuppressionInterval = 0
|
|
98
|
-
|
|
99
|
-
// Keys that are where they are on every layout.
|
|
100
|
-
let fixedKeys: [String: CGKeyCode] = [
|
|
101
|
-
"return": 36, "tab": 48, "space": 49, "backspace": 51, "escape": 53,
|
|
102
|
-
"left": 123, "right": 124, "down": 125, "up": 126, "delete": 117, "home": 115, "end": 119, "pageup": 116, "pagedown": 121,
|
|
103
|
-
"f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97, "f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
func sourceProperty<T>(_ source: TISInputSource, _ key: CFString) -> T? {
|
|
107
|
-
guard let pointer = TISGetInputSourceProperty(source, key) else { return nil }
|
|
108
|
-
return Unmanaged<AnyObject>.fromOpaque(pointer).takeUnretainedValue() as? T
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Where each character is on this Mac's own layout: a shortcut's letter is the key that types it
|
|
112
|
-
// here, not the key it is on an American keyboard.
|
|
113
|
-
var layoutID = ""
|
|
114
|
-
var layoutKeys: [String: CGKeyCode] = [:]
|
|
115
|
-
|
|
116
|
-
func readLayout() {
|
|
117
|
-
guard let layout = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue() else { return }
|
|
118
|
-
let id: String = sourceProperty(layout, kTISPropertyInputSourceID) ?? ""
|
|
119
|
-
if id == layoutID, !layoutKeys.isEmpty { return }
|
|
120
|
-
guard let data: NSData = sourceProperty(layout, kTISPropertyUnicodeKeyLayoutData) else { return }
|
|
121
|
-
layoutID = id
|
|
122
|
-
layoutKeys = [:]
|
|
123
|
-
let table = data.bytes.assumingMemoryBound(to: UCKeyboardLayout.self)
|
|
124
|
-
for code in 0..<127 {
|
|
125
|
-
var dead: UInt32 = 0
|
|
126
|
-
var length = 0
|
|
127
|
-
var characters = [UniChar](repeating: 0, count: 4)
|
|
128
|
-
let status = UCKeyTranslate(table, UInt16(code), UInt16(kUCKeyActionDown), 0, UInt32(LMGetKbdType()), OptionBits(kUCKeyTranslateNoDeadKeysBit), &dead, 4, &length, &characters)
|
|
129
|
-
guard status == noErr, length == 1 else { continue }
|
|
130
|
-
let character = String(utf16CodeUnits: characters, count: 1).lowercased()
|
|
131
|
-
// The main keys come before the number pad's.
|
|
132
|
-
if layoutKeys[character] == nil { layoutKeys[character] = CGKeyCode(code) }
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
func keyCode(_ name: String) -> CGKeyCode? {
|
|
137
|
-
if let fixed = fixedKeys[name] { return fixed }
|
|
138
|
-
readLayout()
|
|
139
|
-
return layoutKeys[name] ?? keyCodes[name]
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// The keys a modifier is, pressed and let go around what it modifies as a hand does: some programs
|
|
143
|
-
// look at the keyboard's state, not at the event's flags.
|
|
144
|
-
let modifierKeys: [(name: String, code: CGKeyCode, flag: CGEventFlags)] = [
|
|
145
|
-
("ctrl", 59, .maskControl), ("alt", 58, .maskAlternate), ("shift", 56, .maskShift), ("cmd", 55, .maskCommand),
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
// Text from the phone is already what it should be. An input method on this Mac (pinyin, say) would
|
|
149
|
-
// take its letters and compose with them again, so while text is coming the Mac types with its plain
|
|
150
|
-
// layout; a moment after the last of it, the input source the user had is back.
|
|
151
|
-
var ownSource: TISInputSource?
|
|
152
|
-
var restore: DispatchWorkItem?
|
|
153
|
-
|
|
154
|
-
func plainWhileTyping() {
|
|
155
|
-
if dryRun { return }
|
|
156
|
-
if let current = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue() {
|
|
157
|
-
let plain: Bool = sourceProperty(current, kTISPropertyInputSourceIsASCIICapable) ?? true
|
|
158
|
-
if !plain, let layout = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue() {
|
|
159
|
-
if ownSource == nil { ownSource = current }
|
|
160
|
-
TISSelectInputSource(layout)
|
|
161
|
-
// The program in front takes a moment to follow.
|
|
162
|
-
usleep(150_000)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
guard ownSource != nil else { return }
|
|
166
|
-
restore?.cancel()
|
|
167
|
-
let work = DispatchWorkItem {
|
|
168
|
-
if let source = ownSource { TISSelectInputSource(source) }
|
|
169
|
-
ownSource = nil
|
|
170
|
-
}
|
|
171
|
-
restore = work
|
|
172
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5, execute: work)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/// The American layout, for a Mac whose own can't be read.
|
|
176
|
-
let keyCodes: [String: CGKeyCode] = [
|
|
177
|
-
"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,
|
|
178
|
-
"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,
|
|
179
|
-
"]": 30, "o": 31, "u": 32, "[": 33, "i": 34, "p": 35, "return": 36, "l": 37, "j": 38, "'": 39, "k": 40, ";": 41, "\\": 42,
|
|
180
|
-
",": 43, "/": 44, "n": 45, "m": 46, ".": 47, "tab": 48, "space": 49, "\u{60}": 50, "backspace": 51, "escape": 53,
|
|
181
|
-
"left": 123, "right": 124, "down": 125, "up": 126, "delete": 117, "home": 115, "end": 119, "pageup": 116, "pagedown": 121,
|
|
182
|
-
"f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97, "f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
|
|
183
|
-
]
|
|
184
|
-
|
|
185
|
-
func flags(_ names: Any?) -> CGEventFlags {
|
|
186
|
-
var result: CGEventFlags = []
|
|
187
|
-
for name in (names as? [String]) ?? [] {
|
|
188
|
-
switch name {
|
|
189
|
-
case "cmd": result.insert(.maskCommand)
|
|
190
|
-
case "shift": result.insert(.maskShift)
|
|
191
|
-
case "alt": result.insert(.maskAlternate)
|
|
192
|
-
case "ctrl": result.insert(.maskControl)
|
|
193
|
-
default: break
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return result
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/// One device watching one display.
|
|
200
|
-
final class Viewer {
|
|
201
|
-
let id: String?
|
|
202
|
-
let screen: Int
|
|
203
|
-
var bounds: CGRect
|
|
204
|
-
var position: CGPoint
|
|
205
|
-
var held: [String: Bool] = [:]
|
|
206
|
-
/// Modifiers this viewer is holding down for a click or a drag.
|
|
207
|
-
var holding: [String] = []
|
|
208
|
-
var lastMove = Date.distantPast
|
|
209
|
-
var lastReported = CGPoint(x: -1, y: -1)
|
|
210
|
-
|
|
211
|
-
init(id: String?, screen: Int) {
|
|
212
|
-
self.id = id
|
|
213
|
-
self.screen = screen
|
|
214
|
-
bounds = Viewer.bounds(of: screen)
|
|
215
|
-
position = CGEvent(source: nil)?.location ?? CGPoint(x: bounds.midX, y: bounds.midY)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/// ffmpeg's "Capture screen N" is the Nth active display.
|
|
219
|
-
static func bounds(of screen: Int) -> CGRect {
|
|
220
|
-
var count: UInt32 = 0
|
|
221
|
-
CGGetActiveDisplayList(0, nil, &count)
|
|
222
|
-
var ids = [CGDirectDisplayID](repeating: 0, count: Int(count))
|
|
223
|
-
CGGetActiveDisplayList(count, &ids, &count)
|
|
224
|
-
return CGDisplayBounds(screen < Int(count) ? ids[screen] : CGMainDisplayID())
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
func tell(_ object: [String: Any]) {
|
|
228
|
-
var message = object
|
|
229
|
-
if let id = id { message["v"] = id }
|
|
230
|
-
emit(message)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
func post(_ event: CGEvent?, _ what: [String: Any]) {
|
|
234
|
-
if dryRun {
|
|
235
|
-
tell(what.merging(["t": "posted"]) { current, _ in current })
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
event?.post(tap: .cghidEventTap)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
func point(_ command: [String: Any]) -> CGPoint? {
|
|
242
|
-
guard let x = command["x"] as? Double, let y = command["y"] as? Double else { return nil }
|
|
243
|
-
// Just inside the far edges: a point on the edge itself belongs to the next display.
|
|
244
|
-
return CGPoint(
|
|
245
|
-
x: bounds.minX + min(max(x, 0), 1) * (bounds.width - 1),
|
|
246
|
-
y: bounds.minY + min(max(y, 0), 1) * (bounds.height - 1)
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
func move(to target: CGPoint) {
|
|
251
|
-
let kind: CGEventType = held["left"] == true ? .leftMouseDragged : held["right"] == true ? .rightMouseDragged : .mouseMoved
|
|
252
|
-
let event = CGEvent(mouseEventSource: source, mouseType: kind, mouseCursorPosition: target, mouseButton: held["right"] == true ? .right : .left)
|
|
253
|
-
event?.setIntegerValueField(.mouseEventDeltaX, value: Int64(target.x - position.x))
|
|
254
|
-
event?.setIntegerValueField(.mouseEventDeltaY, value: Int64(target.y - position.y))
|
|
255
|
-
event?.flags = []
|
|
256
|
-
position = target
|
|
257
|
-
lastMove = Date()
|
|
258
|
-
post(event, ["kind": kind == .mouseMoved ? "move" : "drag", "x": Double(target.x), "y": Double(target.y)])
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/// Presses (or lets go of) modifier keys; the flags held once it has.
|
|
262
|
-
func modifiers(_ names: [String], down: Bool, over base: CGEventFlags = []) -> CGEventFlags {
|
|
263
|
-
var flags = base
|
|
264
|
-
let keys = modifierKeys.filter { names.contains($0.name) }
|
|
265
|
-
for key in down ? keys : keys.reversed() {
|
|
266
|
-
if down { flags.insert(key.flag) } else { flags.remove(key.flag) }
|
|
267
|
-
let event = CGEvent(keyboardEventSource: source, virtualKey: key.code, keyDown: down)
|
|
268
|
-
event?.type = .flagsChanged
|
|
269
|
-
event?.flags = flags
|
|
270
|
-
post(event, ["kind": down ? "moddown" : "modup", "k": key.name])
|
|
271
|
-
}
|
|
272
|
-
return flags
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
func button(_ name: String, down: Bool, clicks: Int64, with names: [String]) {
|
|
276
|
-
if held[name] == down { return }
|
|
277
|
-
held[name] = down
|
|
278
|
-
// Shift-click, command-drag: the keys go down before the button and stay until it is up.
|
|
279
|
-
var modifiers: CGEventFlags = []
|
|
280
|
-
if down {
|
|
281
|
-
holding = names
|
|
282
|
-
modifiers = self.modifiers(names, down: true)
|
|
283
|
-
} else {
|
|
284
|
-
modifiers = flags(holding)
|
|
285
|
-
}
|
|
286
|
-
defer {
|
|
287
|
-
if !down {
|
|
288
|
-
_ = self.modifiers(holding, down: false, over: flags(holding))
|
|
289
|
-
holding = []
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
let right = name == "right"
|
|
293
|
-
let kind: CGEventType = right ? (down ? .rightMouseDown : .rightMouseUp) : (down ? .leftMouseDown : .leftMouseUp)
|
|
294
|
-
let event = CGEvent(mouseEventSource: source, mouseType: kind, mouseCursorPosition: position, mouseButton: right ? .right : .left)
|
|
295
|
-
event?.setIntegerValueField(.mouseEventClickState, value: clicks)
|
|
296
|
-
event?.flags = modifiers
|
|
297
|
-
lastMove = Date()
|
|
298
|
-
post(event, ["kind": down ? "down" : "up", "b": name, "n": clicks, "x": Double(position.x), "y": Double(position.y), "flags": modifiers.rawValue])
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
func key(_ code: CGKeyCode, with names: [String], name: String) {
|
|
302
|
-
let modifiers = self.modifiers(names, down: true)
|
|
303
|
-
for down in [true, false] {
|
|
304
|
-
let event = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: down)
|
|
305
|
-
event?.flags = modifiers
|
|
306
|
-
post(event, ["kind": down ? "keydown" : "keyup", "k": name, "code": Int(code), "flags": modifiers.rawValue])
|
|
307
|
-
}
|
|
308
|
-
_ = self.modifiers(names, down: false, over: modifiers)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
func type(_ text: String) {
|
|
312
|
-
plainWhileTyping()
|
|
313
|
-
// A string a key event: whatever the keyboard layout or input method, the characters arrive as written.
|
|
314
|
-
let units = Array(text.utf16)
|
|
315
|
-
var index = 0
|
|
316
|
-
while index < units.count {
|
|
317
|
-
var end = min(index + 20, units.count)
|
|
318
|
-
// Not between the halves of a surrogate pair.
|
|
319
|
-
if end < units.count, UTF16.isLeadSurrogate(units[end - 1]) { end -= 1 }
|
|
320
|
-
let chunk = Array(units[index..<end])
|
|
321
|
-
for down in [true, false] {
|
|
322
|
-
let event = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: down)
|
|
323
|
-
event?.flags = []
|
|
324
|
-
event?.keyboardSetUnicodeString(stringLength: chunk.count, unicodeString: chunk)
|
|
325
|
-
post(event, ["kind": down ? "textdown" : "textup", "s": String(utf16CodeUnits: chunk, count: chunk.count)])
|
|
326
|
-
}
|
|
327
|
-
index = end
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/// The viewer left: nothing stays pressed.
|
|
332
|
-
func releaseAll() {
|
|
333
|
-
for (name, down) in held where down { button(name, down: false, clicks: 1, with: []) }
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
func handle(_ command: [String: Any]) {
|
|
337
|
-
guard let kind = command["t"] as? String else { return }
|
|
338
|
-
if kind == "prompt" { return askControl() }
|
|
339
|
-
guard trusted() else { return }
|
|
340
|
-
switch kind {
|
|
341
|
-
case "move":
|
|
342
|
-
if let target = point(command) { move(to: target) }
|
|
343
|
-
case "down", "up":
|
|
344
|
-
let name = (command["b"] as? String) == "right" ? "right" : "left"
|
|
345
|
-
let clicks = Int64(min(max((command["n"] as? Int) ?? 1, 1), 3))
|
|
346
|
-
button(name, down: kind == "down", clicks: clicks, with: (command["m"] as? [String]) ?? [])
|
|
347
|
-
case "scroll":
|
|
348
|
-
let dx = Int32(max(min((command["dx"] as? Double) ?? 0, 2000), -2000))
|
|
349
|
-
let dy = Int32(max(min((command["dy"] as? Double) ?? 0, 2000), -2000))
|
|
350
|
-
let event = CGEvent(scrollWheelEvent2Source: source, units: .pixel, wheelCount: 2, wheel1: dy, wheel2: dx, wheel3: 0)
|
|
351
|
-
event?.flags = flags(command["m"])
|
|
352
|
-
post(event, ["kind": "scroll", "dx": Int(dx), "dy": Int(dy)])
|
|
353
|
-
case "key":
|
|
354
|
-
guard let name = command["k"] as? String, let code = keyCode(name) else { return }
|
|
355
|
-
key(code, with: (command["m"] as? [String]) ?? [], name: name)
|
|
356
|
-
case "text":
|
|
357
|
-
if let text = command["s"] as? String { type(text) }
|
|
358
|
-
default:
|
|
359
|
-
break
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/// Where the pointer is when someone at the Mac (or another program) moved it.
|
|
364
|
-
func report() {
|
|
365
|
-
guard Date().timeIntervalSince(lastMove) > 0.3, let location = CGEvent(source: nil)?.location else { return }
|
|
366
|
-
bounds = Viewer.bounds(of: screen)
|
|
367
|
-
if abs(location.x - lastReported.x) < 0.5, abs(location.y - lastReported.y) < 0.5 { return }
|
|
368
|
-
lastReported = location
|
|
369
|
-
position = location
|
|
370
|
-
guard bounds.contains(location) else { return }
|
|
371
|
-
tell(["t": "cursor", "x": Double((location.x - bounds.minX) / max(bounds.width - 1, 1)), "y": Double((location.y - bounds.minY) / max(bounds.height - 1, 1))])
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
func ready() {
|
|
375
|
-
tell(["t": "ready", "trusted": trusted(), "w": Double(bounds.width), "h": Double(bounds.height), "app": responsibleApp()])
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
var viewers: [String: Viewer] = [:]
|
|
380
|
-
var captures: [String: Process] = [:]
|
|
381
|
-
var wasTrusted = trusted()
|
|
382
|
-
|
|
383
|
-
func end(_ process: Process) {
|
|
384
|
-
guard process.isRunning else { return }
|
|
385
|
-
process.terminate()
|
|
386
|
-
// ffmpeg holding a screen can sit through being asked.
|
|
387
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
|
388
|
-
if process.isRunning { kill(process.processIdentifier, SIGKILL) }
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/// Runs a screen capture as this app's own child, so the recording is this app's to be allowed,
|
|
393
|
-
/// with its picture going straight down a socket of the host's.
|
|
394
|
-
func capture(_ id: String, _ command: [String: Any]) {
|
|
395
|
-
let failed = { (reason: String) in emit(["t": "captured", "v": id, "code": -1, "errors": reason]) }
|
|
396
|
-
guard let program = command["exec"] as? String, let args = command["args"] as? [String], let path = command["socket"] as? String else { return failed("not a capture") }
|
|
397
|
-
// The one program this runs for the host.
|
|
398
|
-
guard dryRun || (program as NSString).lastPathComponent == "ffmpeg" else { return failed("not ffmpeg") }
|
|
399
|
-
guard let sink = connect(to: path) else { return failed("the host's socket could not be reached") }
|
|
400
|
-
let process = Process()
|
|
401
|
-
process.executableURL = URL(fileURLWithPath: program)
|
|
402
|
-
process.arguments = args
|
|
403
|
-
process.standardInput = FileHandle.nullDevice
|
|
404
|
-
process.standardOutput = sink
|
|
405
|
-
let errors = Pipe()
|
|
406
|
-
process.standardError = errors
|
|
407
|
-
var said = Data()
|
|
408
|
-
errors.fileHandleForReading.readabilityHandler = { handle in
|
|
409
|
-
let chunk = handle.availableData
|
|
410
|
-
if chunk.isEmpty {
|
|
411
|
-
handle.readabilityHandler = nil
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
DispatchQueue.main.async {
|
|
415
|
-
said.append(chunk)
|
|
416
|
-
if said.count > 2000 { said.removeFirst(said.count - 2000) }
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
process.terminationHandler = { finished in
|
|
420
|
-
DispatchQueue.main.async {
|
|
421
|
-
captures[id] = nil
|
|
422
|
-
try? sink.close()
|
|
423
|
-
emit(["t": "captured", "v": id, "code": Int(finished.terminationStatus), "errors": String(data: said, encoding: .utf8) ?? ""])
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
do {
|
|
427
|
-
try process.run()
|
|
428
|
-
captures[id] = process
|
|
429
|
-
} catch {
|
|
430
|
-
failed(error.localizedDescription)
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/// Lines from a pipe or a socket, each handled on the main queue; then end.
|
|
435
|
-
func readLines(from handle: FileHandle, each: @escaping (String) -> Void, end: @escaping () -> Void) {
|
|
436
|
-
Thread.detachNewThread {
|
|
437
|
-
var buffer = Data()
|
|
438
|
-
while true {
|
|
439
|
-
let chunk = handle.availableData
|
|
440
|
-
if chunk.isEmpty { break }
|
|
441
|
-
buffer.append(chunk)
|
|
442
|
-
while let newline = buffer.firstIndex(of: 10) {
|
|
443
|
-
let line = String(data: buffer.subdata(in: buffer.startIndex..<newline), encoding: .utf8) ?? ""
|
|
444
|
-
buffer.removeSubrange(buffer.startIndex...newline)
|
|
445
|
-
DispatchQueue.main.sync { each(line) }
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
DispatchQueue.main.sync { end() }
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
func object(_ line: String) -> [String: Any]? {
|
|
453
|
-
guard let data = line.data(using: .utf8) else { return nil }
|
|
454
|
-
return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
func watch() -> DispatchSourceTimer {
|
|
458
|
-
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
459
|
-
timer.schedule(deadline: .now(), repeating: .milliseconds(250))
|
|
460
|
-
timer.setEventHandler {
|
|
461
|
-
let now = trusted()
|
|
462
|
-
if now != wasTrusted {
|
|
463
|
-
wasTrusted = now
|
|
464
|
-
emit(["t": "trusted", "on": now])
|
|
465
|
-
}
|
|
466
|
-
for viewer in viewers.values { viewer.report() }
|
|
467
|
-
}
|
|
468
|
-
timer.resume()
|
|
469
|
-
return timer
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
func connect(to path: String) -> FileHandle? {
|
|
473
|
-
let descriptor = socket(AF_UNIX, SOCK_STREAM, 0)
|
|
474
|
-
guard descriptor >= 0 else { return nil }
|
|
475
|
-
var address = sockaddr_un()
|
|
476
|
-
address.sun_family = sa_family_t(AF_UNIX)
|
|
477
|
-
let bytes = Array(path.utf8)
|
|
478
|
-
guard bytes.count < MemoryLayout.size(ofValue: address.sun_path) else { return nil }
|
|
479
|
-
withUnsafeMutablePointer(to: &address.sun_path) { pointer in
|
|
480
|
-
pointer.withMemoryRebound(to: UInt8.self, capacity: bytes.count + 1) { raw in
|
|
481
|
-
for (index, byte) in bytes.enumerated() { raw[index] = byte }
|
|
482
|
-
raw[bytes.count] = 0
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
let size = socklen_t(MemoryLayout<sockaddr_un>.size)
|
|
486
|
-
let result = withUnsafePointer(to: &address) { pointer in
|
|
487
|
-
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { Darwin.connect(descriptor, $0, size) }
|
|
488
|
-
}
|
|
489
|
-
guard result == 0 else {
|
|
490
|
-
close(descriptor)
|
|
491
|
-
return nil
|
|
492
|
-
}
|
|
493
|
-
return FileHandle(fileDescriptor: descriptor, closeOnDealloc: true)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
signal(SIGPIPE, SIG_IGN)
|
|
497
|
-
|
|
498
|
-
if arguments.contains("--status") {
|
|
499
|
-
emit(status())
|
|
500
|
-
exit(0)
|
|
501
|
-
}
|
|
502
|
-
if arguments.contains("--ask-control") {
|
|
503
|
-
askControl()
|
|
504
|
-
emit(status())
|
|
505
|
-
exit(0)
|
|
506
|
-
}
|
|
507
|
-
if arguments.contains("--ask-recording") {
|
|
508
|
-
askRecording()
|
|
509
|
-
emit(status())
|
|
510
|
-
exit(0)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if let path = argument(after: "--connect") {
|
|
514
|
-
// An app of its own, with no window and no place in the Dock.
|
|
515
|
-
guard let link = connect(to: path) else { exit(1) }
|
|
516
|
-
output = link
|
|
517
|
-
emit(status())
|
|
518
|
-
let timer = watch()
|
|
519
|
-
readLines(from: link, each: { line in
|
|
520
|
-
guard let command = object(line), let kind = command["t"] as? String else { return }
|
|
521
|
-
if kind == "status" { return emit(status()) }
|
|
522
|
-
if kind == "ask" { return askControl() }
|
|
523
|
-
if kind == "ask-recording" { return askRecording() }
|
|
524
|
-
guard let id = command["v"] as? String else { return }
|
|
525
|
-
if kind == "capture" {
|
|
526
|
-
capture(id, command)
|
|
527
|
-
} else if kind == "stop" {
|
|
528
|
-
if let process = captures[id] { end(process) }
|
|
529
|
-
} else if kind == "open" {
|
|
530
|
-
viewers[id]?.releaseAll()
|
|
531
|
-
let viewer = Viewer(id: id, screen: (command["screen"] as? Int) ?? 0)
|
|
532
|
-
viewers[id] = viewer
|
|
533
|
-
viewer.ready()
|
|
534
|
-
} else if kind == "close" {
|
|
535
|
-
viewers.removeValue(forKey: id)?.releaseAll()
|
|
536
|
-
} else {
|
|
537
|
-
viewers[id]?.handle(command)
|
|
538
|
-
}
|
|
539
|
-
}, end: {
|
|
540
|
-
// The host went: so does this, and what it was running.
|
|
541
|
-
for viewer in viewers.values { viewer.releaseAll() }
|
|
542
|
-
for process in captures.values where process.isRunning { kill(process.processIdentifier, SIGKILL) }
|
|
543
|
-
timer.cancel()
|
|
544
|
-
exit(0)
|
|
545
|
-
})
|
|
546
|
-
let app = NSApplication.shared
|
|
547
|
-
app.setActivationPolicy(.accessory)
|
|
548
|
-
app.run()
|
|
549
|
-
} else {
|
|
550
|
-
let viewer = Viewer(id: nil, screen: arguments.compactMap { Int($0) }.first ?? 0)
|
|
551
|
-
viewers[""] = viewer
|
|
552
|
-
viewer.ready()
|
|
553
|
-
let timer = watch()
|
|
554
|
-
readLines(from: FileHandle.standardInput, each: { line in
|
|
555
|
-
if let command = object(line) { viewer.handle(command) }
|
|
556
|
-
}, end: {
|
|
557
|
-
viewer.releaseAll()
|
|
558
|
-
timer.cancel()
|
|
559
|
-
exit(0)
|
|
560
|
-
})
|
|
561
|
-
dispatchMain()
|
|
562
|
-
}
|
|
563
|
-
`;
|
|
564
|
-
//# sourceMappingURL=input-helper.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"input-helper.js","sourceRoot":"","sources":["../../../../host/src/input-helper.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,+CAA+C;AAC/C,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,0EAA0E;AAC1E,uEAAuE;AACvE,+EAA+E;AAC/E,8EAA8E;AAC9E,wDAAwD;AAExD,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwiB5C,CAAC"}
|