@desklink/host 0.1.0 → 0.1.1

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.
Files changed (45) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +50 -16
  3. package/bin/desklink-host.mjs +13 -2
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +5 -1
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts.map +1 -1
  8. package/dist/engineProcess.js +17 -5
  9. package/dist/engineProcess.js.map +1 -1
  10. package/dist/protocol.d.ts +42 -5
  11. package/dist/protocol.d.ts.map +1 -1
  12. package/dist/protocol.js +1 -1
  13. package/dist/protocol.js.map +1 -1
  14. package/dist/resolveEngine.d.ts +1 -0
  15. package/dist/resolveEngine.d.ts.map +1 -1
  16. package/dist/resolveEngine.js +16 -1
  17. package/dist/resolveEngine.js.map +1 -1
  18. package/docs/BOUNDARY.md +4 -3
  19. package/docs/PROTOCOL.md +95 -27
  20. package/engine/Cargo.lock +195 -1
  21. package/engine/Cargo.toml +25 -5
  22. package/engine/build.rs +98 -23
  23. package/engine/native/agent_overlay_mac.m +133 -0
  24. package/engine/native/agent_overlay_wayland.c +192 -0
  25. package/engine/native/agent_overlay_x11.c +103 -0
  26. package/engine/native/mac_stream.mm +188 -0
  27. package/engine/src/capture.rs +169 -19
  28. package/engine/src/capture_mac.rs +145 -0
  29. package/engine/src/clipboard_mac.rs +182 -0
  30. package/engine/src/convert.rs +57 -1
  31. package/engine/src/indicator.rs +162 -0
  32. package/engine/src/input.rs +12 -0
  33. package/engine/src/input_mac.rs +312 -0
  34. package/engine/src/keymap.rs +25 -0
  35. package/engine/src/keymap_mac.rs +209 -0
  36. package/engine/src/mac.rs +722 -0
  37. package/engine/src/main.rs +116 -2
  38. package/engine/src/portal.rs +4 -0
  39. package/engine/src/portal_mac.rs +68 -0
  40. package/engine/src/protocol.rs +48 -1
  41. package/engine/src/session.rs +556 -62
  42. package/engine/src/x11.rs +23 -3
  43. package/engine/src/x11_mac.rs +40 -0
  44. package/examples/reference-client.html +33 -4
  45. package/package.json +2 -2
@@ -0,0 +1,162 @@
1
+ //! Local feedback for agent sessions. The helper is a separate nonactivating
2
+ //! window-owning process so its windows can be excluded from desktop capture.
3
+ use anyhow::{Context, Result};
4
+ use std::io::{BufRead, BufReader, Write};
5
+ use std::process::{Child, ChildStdin, Command, Stdio};
6
+
7
+ pub struct Indicator {
8
+ child: Option<Child>,
9
+ input: Option<ChildStdin>,
10
+ scale_x: f64,
11
+ scale_y: f64,
12
+ #[cfg(target_os = "linux")]
13
+ position: std::sync::Arc<std::sync::Mutex<Option<(i64, i64)>>>,
14
+ }
15
+
16
+ impl Indicator {
17
+ #[cfg(target_os = "macos")]
18
+ pub fn start(display_id: u32, width: usize, height: usize) -> Result<Self> {
19
+ let executable = std::env::current_exe()?;
20
+ let (_, _, points_w, points_h) = crate::mac::display_geometry(display_id)
21
+ .context("the selected display is unavailable")?;
22
+ Self::spawn(
23
+ Command::new(executable)
24
+ .arg("agent-overlay")
25
+ .arg(display_id.to_string()),
26
+ points_w / width as f64,
27
+ points_h / height as f64,
28
+ )
29
+ }
30
+
31
+ #[cfg(target_os = "linux")]
32
+ pub fn start(display: &str, width: usize, height: usize) -> Result<Self> {
33
+ let executable = std::env::current_exe()?;
34
+ let _ = (width, height);
35
+ Self::spawn(
36
+ Command::new(executable).arg("agent-overlay").arg(display),
37
+ 1.0,
38
+ 1.0,
39
+ )
40
+ }
41
+
42
+ #[cfg(target_os = "linux")]
43
+ pub fn start_wayland(width: usize, height: usize) -> Result<Self> {
44
+ let executable = std::env::current_exe()?;
45
+ Self::spawn(
46
+ Command::new(executable)
47
+ .arg("agent-overlay-wayland")
48
+ .arg(width.to_string())
49
+ .arg(height.to_string()),
50
+ 1.0,
51
+ 1.0,
52
+ )
53
+ }
54
+
55
+ #[cfg(target_os = "linux")]
56
+ pub fn position(&self) -> std::sync::Arc<std::sync::Mutex<Option<(i64, i64)>>> {
57
+ self.position.clone()
58
+ }
59
+
60
+ fn spawn(command: &mut Command, scale_x: f64, scale_y: f64) -> Result<Self> {
61
+ let mut child = command
62
+ .stdin(Stdio::piped())
63
+ .stdout(Stdio::piped())
64
+ .stderr(Stdio::inherit())
65
+ .spawn()
66
+ .context("could not start agent indicator")?;
67
+ let stdout = child.stdout.take().unwrap();
68
+ let (ready_tx, ready_rx) = std::sync::mpsc::channel();
69
+ let reader = std::thread::spawn(move || {
70
+ let mut ready = String::new();
71
+ let result = BufReader::new(stdout).read_line(&mut ready);
72
+ let _ = ready_tx.send(result.is_ok() && ready.trim() == "READY");
73
+ });
74
+ if ready_rx.recv_timeout(std::time::Duration::from_secs(5)) != Ok(true) {
75
+ let _ = child.kill();
76
+ let _ = child.wait();
77
+ let _ = reader.join();
78
+ anyhow::bail!("agent indicator did not open its overlay within 5s");
79
+ }
80
+ let _ = reader.join();
81
+ Ok(Self {
82
+ input: child.stdin.take(),
83
+ child: Some(child),
84
+ scale_x,
85
+ scale_y,
86
+ #[cfg(target_os = "linux")]
87
+ position: Default::default(),
88
+ })
89
+ }
90
+
91
+ #[cfg(target_os = "macos")]
92
+ pub fn pid(&self) -> u32 {
93
+ self.child.as_ref().unwrap().id()
94
+ }
95
+
96
+ pub fn event(&mut self, kind: char, x: i64, y: i64) {
97
+ if let Some(input) = &mut self.input {
98
+ let (x, y) = (x as f64 * self.scale_x, y as f64 * self.scale_y);
99
+ if writeln!(input, "{kind} {x:.2} {y:.2}").is_err() {
100
+ self.input = None;
101
+ }
102
+ #[cfg(target_os = "linux")]
103
+ if x >= 0.0 && y >= 0.0 {
104
+ if let Ok(mut position) = self.position.lock() {
105
+ *position = Some((x as i64, y as i64));
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ impl Drop for Indicator {
113
+ fn drop(&mut self) {
114
+ // Closing stdin fades the edge and exits; do not wait for the animation.
115
+ self.input.take();
116
+ if let Some(mut child) = self.child.take() {
117
+ std::thread::spawn(move || {
118
+ let _ = child.wait();
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ #[cfg(all(test, target_os = "linux"))]
125
+ mod tests {
126
+ unsafe extern "C" {
127
+ fn desklink_wayland_geometry(
128
+ source_w: i32,
129
+ source_h: i32,
130
+ surface_w: i32,
131
+ surface_h: i32,
132
+ px: f64,
133
+ py: f64,
134
+ x: *mut f64,
135
+ y: *mut f64,
136
+ ) -> i32;
137
+ fn desklink_wayland_policy(
138
+ anchor: *mut u32,
139
+ keyboard_focus: *mut u32,
140
+ empty_input: *mut u32,
141
+ );
142
+ }
143
+
144
+ #[test]
145
+ fn layer_shell_maps_source_pixels_and_never_takes_input_or_focus() {
146
+ let (mut x, mut y) = (0.0, 0.0);
147
+ unsafe {
148
+ assert_eq!(
149
+ desklink_wayland_geometry(3840, 2160, 1920, 1080, 1000.0, 600.0, &mut x, &mut y),
150
+ 1
151
+ );
152
+ assert_eq!((x, y), (500.0, 300.0));
153
+ assert_eq!(
154
+ desklink_wayland_geometry(0, 2160, 1920, 1080, 10.0, 10.0, &mut x, &mut y),
155
+ 0
156
+ );
157
+ let (mut anchor, mut keyboard, mut empty) = (0, 1, 0);
158
+ desklink_wayland_policy(&mut anchor, &mut keyboard, &mut empty);
159
+ assert_eq!((anchor, keyboard, empty), (15, 0, 1));
160
+ }
161
+ }
162
+ }
@@ -108,6 +108,18 @@ pub mod keycode {
108
108
  pub const PAGE_UP: i16 = 104;
109
109
  pub const PAGE_DOWN: i16 = 109;
110
110
  pub const DELETE: i16 = 111;
111
+ pub const F1: i16 = 59;
112
+ pub const F2: i16 = 60;
113
+ pub const F3: i16 = 61;
114
+ pub const F4: i16 = 62;
115
+ pub const F5: i16 = 63;
116
+ pub const F6: i16 = 64;
117
+ pub const F7: i16 = 65;
118
+ pub const F8: i16 = 66;
119
+ pub const F9: i16 = 67;
120
+ pub const F10: i16 = 68;
121
+ pub const F11: i16 = 87;
122
+ pub const F12: i16 = 88;
111
123
  }
112
124
 
113
125
  /// Why the input backend is unavailable, so the consumer can say something true
@@ -0,0 +1,312 @@
1
+ use anyhow::{bail, Result};
2
+ use std::os::raw::c_void;
3
+
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ pub enum Button {
6
+ Left,
7
+ Middle,
8
+ Right,
9
+ }
10
+
11
+ pub mod keycode {
12
+ pub const LEFT_CTRL: i16 = 59;
13
+ pub const LEFT_SHIFT: i16 = 56;
14
+ pub const LEFT_ALT: i16 = 58;
15
+ pub const LEFT_META: i16 = 55;
16
+ }
17
+
18
+ impl Button {
19
+ pub fn from_number(number: i64) -> Self {
20
+ match number {
21
+ 2 => Self::Middle,
22
+ 3 => Self::Right,
23
+ _ => Self::Left,
24
+ }
25
+ }
26
+ }
27
+
28
+ #[derive(Debug, Default, Clone, PartialEq, Eq)]
29
+ pub struct HeldState {
30
+ keys: Vec<i16>,
31
+ buttons: Vec<Button>,
32
+ }
33
+
34
+ impl HeldState {
35
+ pub fn key(&mut self, code: i16, down: bool) {
36
+ if down {
37
+ if !self.keys.contains(&code) {
38
+ self.keys.push(code);
39
+ }
40
+ } else {
41
+ self.keys.retain(|held| *held != code);
42
+ }
43
+ }
44
+
45
+ pub fn button(&mut self, button: Button, down: bool) {
46
+ if down {
47
+ if !self.buttons.contains(&button) {
48
+ self.buttons.push(button);
49
+ }
50
+ } else {
51
+ self.buttons.retain(|held| *held != button);
52
+ }
53
+ }
54
+
55
+ pub fn release_plan(&mut self) -> (Vec<Button>, Vec<i16>) {
56
+ (
57
+ std::mem::take(&mut self.buttons),
58
+ std::mem::take(&mut self.keys),
59
+ )
60
+ }
61
+ }
62
+
63
+ #[derive(Debug, Clone, serde::Serialize)]
64
+ pub struct InputUnavailable {
65
+ pub reason: String,
66
+ pub remedy: String,
67
+ }
68
+
69
+ pub fn probe() -> Result<(), InputUnavailable> {
70
+ if unsafe { crate::mac::CGPreflightPostEventAccess() } {
71
+ Ok(())
72
+ } else {
73
+ let app = crate::mac::tcc_responsible_app_name();
74
+ Err(InputUnavailable {
75
+ reason: format!("Accessibility permission has not been granted to {app}."),
76
+ remedy: format!("Allow {app} in System Settings › Privacy & Security › Accessibility, then reconnect."),
77
+ })
78
+ }
79
+ }
80
+
81
+ pub fn request_access() -> bool {
82
+ let key = unsafe { kAXTrustedCheckOptionPrompt };
83
+ let value = unsafe { kCFBooleanTrue };
84
+ let keys = [key];
85
+ let values = [value];
86
+ let options = unsafe {
87
+ CFDictionaryCreate(
88
+ kCFAllocatorDefault,
89
+ keys.as_ptr(),
90
+ values.as_ptr(),
91
+ 1,
92
+ &kCFTypeDictionaryKeyCallBacks,
93
+ &kCFTypeDictionaryValueCallBacks,
94
+ )
95
+ };
96
+ if options.is_null() {
97
+ return false;
98
+ }
99
+ let trusted = unsafe { AXIsProcessTrustedWithOptions(options) };
100
+ unsafe { CFRelease(options) };
101
+ trusted
102
+ }
103
+
104
+ fn modifier_flag(code: i16) -> u64 {
105
+ match code {
106
+ keycode::LEFT_SHIFT => 0x20000,
107
+ keycode::LEFT_CTRL => 0x40000,
108
+ keycode::LEFT_ALT => 0x80000,
109
+ keycode::LEFT_META => 0x100000,
110
+ _ => 0,
111
+ }
112
+ }
113
+
114
+ pub fn native_keycode(code: i16) -> Result<i16> {
115
+ anyhow::ensure!(
116
+ (0..=127).contains(&code),
117
+ "invalid macOS virtual key code {code}"
118
+ );
119
+ Ok(code)
120
+ }
121
+
122
+ #[repr(C)]
123
+ #[derive(Clone, Copy)]
124
+ struct Point {
125
+ x: f64,
126
+ y: f64,
127
+ }
128
+
129
+ type EventRef = *const c_void;
130
+ type SourceRef = *const c_void;
131
+ type CfTypeRef = *const c_void;
132
+
133
+ #[link(name = "ApplicationServices", kind = "framework")]
134
+ unsafe extern "C" {
135
+ fn AXIsProcessTrustedWithOptions(options: CfTypeRef) -> bool;
136
+ static kAXTrustedCheckOptionPrompt: CfTypeRef;
137
+ }
138
+
139
+ #[link(name = "ApplicationServices", kind = "framework")]
140
+ unsafe extern "C" {
141
+ fn CGEventCreateMouseEvent(
142
+ source: SourceRef,
143
+ event_type: u32,
144
+ point: Point,
145
+ button: u32,
146
+ ) -> EventRef;
147
+ fn CGEventCreateScrollWheelEvent(
148
+ source: SourceRef,
149
+ units: u32,
150
+ wheel_count: u32,
151
+ vertical: i32,
152
+ horizontal: i32,
153
+ ) -> EventRef;
154
+ fn CGEventCreateKeyboardEvent(source: SourceRef, keycode: u16, key_down: bool) -> EventRef;
155
+ fn CGEventKeyboardSetUnicodeString(event: EventRef, length: u32, text: *const u16);
156
+ fn CGEventSetFlags(event: EventRef, flags: u64);
157
+ fn CGEventSetLocation(event: EventRef, location: Point);
158
+ fn CGEventPost(tap: u32, event: EventRef);
159
+ }
160
+
161
+ #[link(name = "CoreFoundation", kind = "framework")]
162
+ unsafe extern "C" {
163
+ fn CFDictionaryCreate(
164
+ allocator: CfTypeRef,
165
+ keys: *const CfTypeRef,
166
+ values: *const CfTypeRef,
167
+ count: isize,
168
+ key_callbacks: *const u8,
169
+ value_callbacks: *const u8,
170
+ ) -> CfTypeRef;
171
+ fn CFRelease(value: CfTypeRef);
172
+ static kCFAllocatorDefault: CfTypeRef;
173
+ static kCFBooleanTrue: CfTypeRef;
174
+ static kCFTypeDictionaryKeyCallBacks: u8;
175
+ static kCFTypeDictionaryValueCallBacks: u8;
176
+ }
177
+
178
+ pub struct InputDevices {
179
+ pixel_width: i32,
180
+ pixel_height: i32,
181
+ origin: (f64, f64),
182
+ points_per_pixel: (f64, f64),
183
+ last_position: Point,
184
+ wheel_rest: (f64, f64),
185
+ modifier_flags: u64,
186
+ }
187
+
188
+ unsafe impl Send for InputDevices {}
189
+
190
+ impl InputDevices {
191
+ pub fn create_for_display(width: i32, height: i32, display_id: u32) -> Result<Self> {
192
+ if !request_access() && !unsafe { crate::mac::CGPreflightPostEventAccess() } {
193
+ let app = crate::mac::tcc_responsible_app_name();
194
+ bail!("Accessibility permission is required; allow {app} in System Settings › Privacy & Security › Accessibility");
195
+ }
196
+ let (origin_x, origin_y, point_width, point_height) =
197
+ crate::mac::display_geometry(display_id)
198
+ .ok_or_else(|| anyhow::anyhow!("the selected display is no longer active"))?;
199
+ anyhow::ensure!(
200
+ width > 0 && height > 0,
201
+ "display has invalid pixel dimensions"
202
+ );
203
+ let origin = (origin_x, origin_y);
204
+ Ok(Self {
205
+ pixel_width: width,
206
+ pixel_height: height,
207
+ origin,
208
+ points_per_pixel: (point_width / width as f64, point_height / height as f64),
209
+ last_position: Point {
210
+ x: origin_x,
211
+ y: origin_y,
212
+ },
213
+ wheel_rest: (0.0, 0.0),
214
+ modifier_flags: 0,
215
+ })
216
+ }
217
+
218
+ pub fn move_absolute(&mut self, x: i64, y: i64) {
219
+ let x = x.clamp(0, self.pixel_width as i64 - 1);
220
+ let y = y.clamp(0, self.pixel_height as i64 - 1);
221
+ let point = Point {
222
+ x: self.origin.0 + x as f64 * self.points_per_pixel.0,
223
+ y: self.origin.1 + y as f64 * self.points_per_pixel.1,
224
+ };
225
+ self.last_position = point;
226
+ self.post_mouse(5, point, 0);
227
+ }
228
+
229
+ pub fn button(&mut self, button: Button, down: bool) {
230
+ let (event_type, native_button) = match (button, down) {
231
+ (Button::Left, true) => (1, 0),
232
+ (Button::Left, false) => (2, 0),
233
+ (Button::Right, true) => (3, 1),
234
+ (Button::Right, false) => (4, 1),
235
+ (Button::Middle, true) => (25, 2),
236
+ (Button::Middle, false) => (26, 2),
237
+ };
238
+ self.post_mouse(event_type, self.last_position, native_button);
239
+ }
240
+
241
+ pub fn scroll(&mut self, dx: f64, dy: f64) {
242
+ let (x, y) = (self.wheel_rest.0 + dx, self.wheel_rest.1 + dy);
243
+ let whole_x = x.trunc();
244
+ let whole_y = y.trunc();
245
+ self.wheel_rest = (x - whole_x, y - whole_y);
246
+ if whole_x == 0.0 && whole_y == 0.0 {
247
+ return;
248
+ }
249
+ let event = unsafe {
250
+ CGEventCreateScrollWheelEvent(std::ptr::null(), 0, 2, -whole_y as i32, whole_x as i32)
251
+ };
252
+ if !event.is_null() {
253
+ unsafe { CGEventSetLocation(event, self.last_position) };
254
+ }
255
+ Self::post(event);
256
+ }
257
+
258
+ pub fn key(&mut self, code: i16, down: bool) -> Result<()> {
259
+ let code = native_keycode(code)? as u16;
260
+ let flag = modifier_flag(code as i16);
261
+ if down {
262
+ self.modifier_flags |= flag;
263
+ } else {
264
+ self.modifier_flags &= !flag;
265
+ }
266
+ let event = unsafe { CGEventCreateKeyboardEvent(std::ptr::null(), code, down) };
267
+ if !event.is_null() {
268
+ unsafe { CGEventSetFlags(event, self.modifier_flags) };
269
+ }
270
+ Self::post(event);
271
+ Ok(())
272
+ }
273
+
274
+ pub fn unicode_text(&mut self, text: &str) -> Result<()> {
275
+ let characters: Vec<u16> = text.encode_utf16().collect();
276
+ let event = unsafe { CGEventCreateKeyboardEvent(std::ptr::null(), 0, true) };
277
+ if event.is_null() {
278
+ bail!("CoreGraphics could not create a Unicode key event");
279
+ }
280
+ unsafe {
281
+ CGEventKeyboardSetUnicodeString(event, characters.len() as u32, characters.as_ptr());
282
+ CGEventSetFlags(event, self.modifier_flags);
283
+ }
284
+ Self::post(event);
285
+ Self::post(unsafe { CGEventCreateKeyboardEvent(std::ptr::null(), 0, false) });
286
+ Ok(())
287
+ }
288
+
289
+ fn post_mouse(&self, event_type: u32, point: Point, button: u32) {
290
+ Self::post(unsafe { CGEventCreateMouseEvent(std::ptr::null(), event_type, point, button) });
291
+ }
292
+
293
+ fn post(event: EventRef) {
294
+ if !event.is_null() {
295
+ unsafe {
296
+ CGEventPost(0, event);
297
+ CFRelease(event);
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ #[cfg(test)]
304
+ mod tests {
305
+ use super::*;
306
+
307
+ #[test]
308
+ fn command_key_uses_quartz_command_flag() {
309
+ assert_eq!(modifier_flag(keycode::LEFT_META), 0x100000);
310
+ assert_eq!(modifier_flag(0), 0);
311
+ }
312
+ }
@@ -72,6 +72,18 @@ pub fn named_key(name: &str) -> Option<Keystroke> {
72
72
  "End" => keycode::END,
73
73
  "PageUp" => keycode::PAGE_UP,
74
74
  "PageDown" => keycode::PAGE_DOWN,
75
+ "F1" => keycode::F1,
76
+ "F2" => keycode::F2,
77
+ "F3" => keycode::F3,
78
+ "F4" => keycode::F4,
79
+ "F5" => keycode::F5,
80
+ "F6" => keycode::F6,
81
+ "F7" => keycode::F7,
82
+ "F8" => keycode::F8,
83
+ "F9" => keycode::F9,
84
+ "F10" => keycode::F10,
85
+ "F11" => keycode::F11,
86
+ "F12" => keycode::F12,
75
87
  _ => return None,
76
88
  };
77
89
  Some(Keystroke::simple(code))
@@ -309,7 +321,20 @@ mod tests {
309
321
  fn named_keys_and_modifiers_resolve_to_linux_key_codes() {
310
322
  assert_eq!(named_key("Enter").unwrap().code, keycode::ENTER);
311
323
  assert_eq!(named_key("ArrowLeft").unwrap().code, keycode::LEFT);
324
+ for (name, code) in [
325
+ ("Delete", keycode::DELETE), ("Home", keycode::HOME), ("End", keycode::END),
326
+ ("PageUp", keycode::PAGE_UP), ("PageDown", keycode::PAGE_DOWN),
327
+ ("F1", keycode::F1), ("F2", keycode::F2), ("F3", keycode::F3),
328
+ ("F4", keycode::F4), ("F5", keycode::F5), ("F6", keycode::F6),
329
+ ("F7", keycode::F7), ("F8", keycode::F8), ("F9", keycode::F9),
330
+ ("F10", keycode::F10), ("F11", keycode::F11), ("F12", keycode::F12),
331
+ ] {
332
+ assert_eq!(named_key(name).unwrap().code, code);
333
+ assert!(crate::input::native_keycode(code).is_ok(), "{name} must reach inputtino");
334
+ }
312
335
  assert_eq!(modifier_key("Control"), Some(keycode::LEFT_CTRL));
336
+ assert_eq!(modifier_key("Alt"), Some(keycode::LEFT_ALT));
337
+ assert_eq!(modifier_key("Super"), Some(keycode::LEFT_META));
313
338
  assert!(named_key("Frobnicate").is_none());
314
339
  assert!(modifier_key("Hyper").is_none());
315
340
  }