@desklink/host 0.3.0 → 0.3.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.
@@ -13,7 +13,30 @@ pub struct Indicator {
13
13
  position: std::sync::Arc<std::sync::Mutex<Option<(i64, i64)>>>,
14
14
  }
15
15
 
16
+ #[cfg(target_os = "linux")]
17
+ fn point_wayland_executable() -> Result<std::path::PathBuf> {
18
+ #[cfg(test)]
19
+ if let Some(path) = std::env::var_os("DESKLINK_POINT_TEST_ENGINE") {
20
+ return Ok(path.into());
21
+ }
22
+ Ok(std::env::current_exe()?)
23
+ }
24
+
16
25
  impl Indicator {
26
+ #[cfg(target_os = "linux")]
27
+ pub fn wayland_layer_shell_available() -> Result<bool> {
28
+ let mut command = Command::new(point_wayland_executable()?);
29
+ command.arg("point-layer-shell-probe");
30
+ match Self::spawn(&mut command, 1.0, 1.0) {
31
+ Ok(helper) => {
32
+ helper.stop();
33
+ Ok(true)
34
+ }
35
+ Err(error) if error.to_string().contains("layer_shell_unavailable") => Ok(false),
36
+ Err(error) => Err(error),
37
+ }
38
+ }
39
+
17
40
  #[cfg(target_os = "macos")]
18
41
  pub fn start(display_id: u32, width: usize, height: usize) -> Result<Self> {
19
42
  let executable = std::env::current_exe()?;
@@ -39,6 +62,77 @@ impl Indicator {
39
62
  )
40
63
  }
41
64
 
65
+ #[cfg(target_os = "linux")]
66
+ pub fn start_point(display: &str) -> Result<Self> {
67
+ Self::spawn(
68
+ Command::new(std::env::current_exe()?)
69
+ .arg("point-overlay")
70
+ .arg(display),
71
+ 1.0,
72
+ 1.0,
73
+ )
74
+ }
75
+
76
+ #[cfg(target_os = "linux")]
77
+ pub fn start_point_wayland(
78
+ width: i32,
79
+ height: i32,
80
+ position: Option<(i32, i32)>,
81
+ ) -> Result<Self> {
82
+ let (x, y) = position.unwrap_or_default();
83
+ Self::spawn(
84
+ Command::new(point_wayland_executable()?)
85
+ .arg("point-overlay-wayland")
86
+ .args([
87
+ width.to_string(),
88
+ height.to_string(),
89
+ i32::from(position.is_some()).to_string(),
90
+ x.to_string(),
91
+ y.to_string(),
92
+ ]),
93
+ 1.0,
94
+ 1.0,
95
+ )
96
+ }
97
+
98
+ #[cfg(target_os = "macos")]
99
+ pub fn start_point(display_id: u32, width: usize, height: usize) -> Result<Self> {
100
+ let (_, _, points_w, points_h) = crate::mac::display_geometry(display_id)
101
+ .context("the selected display is unavailable")?;
102
+ Self::spawn(
103
+ Command::new(std::env::current_exe()?)
104
+ .arg("point-overlay")
105
+ .arg(display_id.to_string()),
106
+ points_w / width as f64,
107
+ points_h / height as f64,
108
+ )
109
+ }
110
+
111
+ #[cfg(all(test, target_os = "linux"))]
112
+ pub fn test_pid(&self) -> u32 {
113
+ self.child.as_ref().unwrap().id()
114
+ }
115
+
116
+ pub fn point(&mut self, x: i64, y: i64, timeout_ms: u64, label: &str) -> Result<()> {
117
+ let input = self.input.as_mut().context("point overlay exited")?;
118
+ writeln!(
119
+ input,
120
+ "P {:.2} {:.2} {timeout_ms} {label}",
121
+ x as f64 * self.scale_x,
122
+ y as f64 * self.scale_y
123
+ )?;
124
+ Ok(())
125
+ }
126
+
127
+ /// Point markers have no fade: close/clear must remove them and reap the helper.
128
+ pub fn stop(mut self) {
129
+ self.input.take();
130
+ if let Some(mut child) = self.child.take() {
131
+ let _ = child.kill();
132
+ let _ = child.wait();
133
+ }
134
+ }
135
+
42
136
  #[cfg(target_os = "linux")]
43
137
  pub fn start_wayland(width: usize, height: usize) -> Result<Self> {
44
138
  let executable = std::env::current_exe()?;
@@ -69,13 +163,20 @@ impl Indicator {
69
163
  let reader = std::thread::spawn(move || {
70
164
  let mut ready = String::new();
71
165
  let result = BufReader::new(stdout).read_line(&mut ready);
72
- let _ = ready_tx.send(result.is_ok() && ready.trim() == "READY");
166
+ let _ = ready_tx.send(if result.is_ok() {
167
+ ready.trim().to_owned()
168
+ } else {
169
+ String::new()
170
+ });
73
171
  });
74
- if ready_rx.recv_timeout(std::time::Duration::from_secs(5)) != Ok(true) {
172
+ let ready = ready_rx
173
+ .recv_timeout(std::time::Duration::from_secs(5))
174
+ .unwrap_or_default();
175
+ if ready != "READY" {
75
176
  let _ = child.kill();
76
177
  let _ = child.wait();
77
178
  let _ = reader.join();
78
- anyhow::bail!("agent indicator did not open its overlay within 5s");
179
+ anyhow::bail!("agent indicator did not open its overlay within 5s: {ready}");
79
180
  }
80
181
  let _ = reader.join();
81
182
  Ok(Self {
@@ -0,0 +1,91 @@
1
+ use anyhow::{bail, Result};
2
+
3
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
4
+ pub enum Button {
5
+ Left,
6
+ Middle,
7
+ Right,
8
+ }
9
+
10
+ impl Button {
11
+ pub fn from_number(number: i64) -> Self {
12
+ match number {
13
+ 2 => Self::Middle,
14
+ 3 => Self::Right,
15
+ _ => Self::Left,
16
+ }
17
+ }
18
+ }
19
+
20
+ #[derive(Debug, Default, Clone, PartialEq, Eq)]
21
+ pub struct HeldState {
22
+ keys: Vec<i16>,
23
+ buttons: Vec<Button>,
24
+ }
25
+
26
+ impl HeldState {
27
+ pub fn key(&mut self, code: i16, down: bool) {
28
+ if down {
29
+ if !self.keys.contains(&code) {
30
+ self.keys.push(code);
31
+ }
32
+ } else {
33
+ self.keys.retain(|held| *held != code);
34
+ }
35
+ }
36
+
37
+ pub fn button(&mut self, button: Button, down: bool) {
38
+ if down {
39
+ if !self.buttons.contains(&button) {
40
+ self.buttons.push(button);
41
+ }
42
+ } else {
43
+ self.buttons.retain(|held| *held != button);
44
+ }
45
+ }
46
+
47
+ pub fn release_plan(&mut self) -> (Vec<Button>, Vec<i16>) {
48
+ (
49
+ std::mem::take(&mut self.buttons),
50
+ std::mem::take(&mut self.keys),
51
+ )
52
+ }
53
+ }
54
+
55
+ #[derive(Debug, Clone, serde::Serialize)]
56
+ pub struct InputUnavailable {
57
+ pub reason: String,
58
+ pub remedy: String,
59
+ }
60
+
61
+ pub fn unavailable_reason() -> &'static str {
62
+ "Windows input is not implemented in this build."
63
+ }
64
+ pub fn probe() -> Result<(), InputUnavailable> {
65
+ Err(InputUnavailable {
66
+ reason: unavailable_reason().into(),
67
+ remedy: "Use a future Windows input-enabled build.".into(),
68
+ })
69
+ }
70
+ // This private, unconstructible adapter cannot inject native input.
71
+ pub struct InputDevices {
72
+ _private: (),
73
+ }
74
+ impl InputDevices {
75
+ pub fn create_for_display(_width: i32, _height: i32, _display: u32) -> Result<Self> {
76
+ bail!("{}", unavailable_reason())
77
+ }
78
+ pub fn move_absolute(&mut self, _x: i64, _y: i64) {}
79
+ pub fn button(&mut self, _button: Button, _down: bool) {}
80
+ pub fn scroll(&mut self, _dx: f64, _dy: f64) {}
81
+ pub fn key(&mut self, _code: i16, _down: bool) -> Result<()> {
82
+ bail!("{}", unavailable_reason())
83
+ }
84
+ pub fn unicode_text(&mut self, _text: &str) -> Result<()> {
85
+ bail!("{}", unavailable_reason())
86
+ }
87
+ }
88
+
89
+ pub fn native_keycode(_code: i16) -> Result<i16> {
90
+ bail!("{}", unavailable_reason())
91
+ }
@@ -0,0 +1,39 @@
1
+ use anyhow::Result;
2
+
3
+ // No key mapping is advertised until the native input lane is implemented.
4
+ pub struct Keystroke {
5
+ pub code: i16,
6
+ }
7
+ impl Keystroke {
8
+ pub fn modifiers(&self) -> Vec<i16> {
9
+ Vec::new()
10
+ }
11
+ }
12
+
13
+ pub fn named_key(_name: &str) -> Option<Keystroke> {
14
+ None
15
+ }
16
+ pub fn modifier_key(_name: &str) -> Option<i16> {
17
+ None
18
+ }
19
+ pub fn keyboard_layout() -> &'static str {
20
+ "unknown"
21
+ }
22
+ pub struct LayoutNames;
23
+ impl LayoutNames {
24
+ pub fn from_environment() -> Self {
25
+ Self
26
+ }
27
+ pub fn identity(&self) -> String {
28
+ keyboard_layout().into()
29
+ }
30
+ }
31
+ pub struct Layout;
32
+ impl Layout {
33
+ pub fn from_environment() -> Result<Self> {
34
+ Ok(Self)
35
+ }
36
+ pub fn keystroke_for_char(&self, _character: char) -> Option<Keystroke> {
37
+ None
38
+ }
39
+ }
package/engine/src/mac.rs CHANGED
@@ -26,6 +26,7 @@ struct CGRect {
26
26
  type DisplayId = u32;
27
27
 
28
28
  unsafe extern "C" {
29
+ fn desklink_point_overlay_main(display_id: i32) -> i32;
29
30
  fn desklink_agent_overlay_main(display_id: i32) -> i32;
30
31
  fn desklink_image_bgra(image: *const c_void, length: *mut usize, stride: *mut usize)
31
32
  -> *mut u8;
@@ -182,6 +183,7 @@ fn capabilities() -> Value {
182
183
  "backends": ["screencapturekit"],
183
184
  "formats": ["bgra"],
184
185
  "cursor": "embedded",
186
+ "cursor_modes": ["embedded"],
185
187
  "audio": false,
186
188
  "displays": displays(),
187
189
  "grant": if capture_granted { "granted" } else { "missing-screen-recording" },
@@ -463,14 +465,25 @@ fn indicator_demo(display_id: u32, path: &str) -> Result<(), String> {
463
465
  fn record_agent_overlay(path: &str) -> Result<(), String> {
464
466
  use std::io::Write;
465
467
  let display = unsafe { CGMainDisplayID() };
466
- let (w, h) = crate::convert::fit(unsafe { CGDisplayPixelsWide(display) },
467
- unsafe { CGDisplayPixelsHigh(display) }, 960, 640);
468
+ let (w, h) = crate::convert::fit(
469
+ unsafe { CGDisplayPixelsWide(display) },
470
+ unsafe { CGDisplayPixelsHigh(display) },
471
+ 960,
472
+ 640,
473
+ );
468
474
  let mut file = std::fs::File::create(path).map_err(|error| error.to_string())?;
469
475
  for _ in 0..30 {
470
476
  let (pixels, sw, sh, stride) = capture_display(display)?;
471
- let raw = crate::convert::to_bgrx(&pixels, sw, sh, stride,
472
- crate::convert::PixelFormat::Bgra, w, h)
473
- .ok_or_else(|| String::from("could not scale recording frame"))?;
477
+ let raw = crate::convert::to_bgrx(
478
+ &pixels,
479
+ sw,
480
+ sh,
481
+ stride,
482
+ crate::convert::PixelFormat::Bgra,
483
+ w,
484
+ h,
485
+ )
486
+ .ok_or_else(|| String::from("could not scale recording frame"))?;
474
487
  file.write_all(&raw).map_err(|error| error.to_string())?;
475
488
  std::thread::sleep(Duration::from_millis(100));
476
489
  }
@@ -581,7 +594,12 @@ pub fn run() -> i32 {
581
594
  let command = args.next().unwrap_or_else(|| "help".into());
582
595
  if matches!(
583
596
  command.as_str(),
584
- "serve" | "capabilities" | "capture-probe" | "indicator-demo" | "axi-record" | "setup-input"
597
+ "serve"
598
+ | "capabilities"
599
+ | "capture-probe"
600
+ | "indicator-demo"
601
+ | "axi-record"
602
+ | "setup-input"
585
603
  ) {
586
604
  #[cfg(not(desklink_macos_cli))]
587
605
  if let Err(error) = ensure_disclaimed() {
@@ -599,6 +617,10 @@ pub fn run() -> i32 {
599
617
  .status();
600
618
  return match result { Ok(status) => status.code().unwrap_or(1), Err(error) => { eprintln!("axi bridge: {error}"); 1 } };
601
619
  }
620
+ "point-overlay" => {
621
+ let display = args.next().and_then(|value| value.parse().ok()).unwrap_or(0);
622
+ return unsafe { desklink_point_overlay_main(display) };
623
+ }
602
624
  "agent-overlay" => {
603
625
  let display = args.next().and_then(|value| value.parse().ok()).unwrap_or(0);
604
626
  return unsafe { desklink_agent_overlay_main(display) };
@@ -11,18 +11,29 @@ mod capture;
11
11
  #[cfg(target_os = "macos")]
12
12
  #[path = "capture_mac.rs"]
13
13
  mod capture;
14
+ #[cfg(target_os = "windows")]
15
+ #[path = "capture_win.rs"]
16
+ mod capture;
14
17
  #[cfg(target_os = "linux")]
15
18
  mod clipboard;
16
19
  #[cfg(target_os = "macos")]
17
20
  #[path = "clipboard_mac.rs"]
18
21
  mod clipboard;
19
- #[cfg(any(target_os = "linux", target_os = "macos"))]
22
+ #[cfg(target_os = "windows")]
23
+ #[path = "clipboard_win.rs"]
24
+ mod clipboard;
25
+ #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
20
26
  mod convert;
21
27
  #[cfg(target_os = "linux")]
28
+ mod cursor;
29
+ #[cfg(target_os = "linux")]
22
30
  mod encoder;
23
- #[cfg(all(target_os = "macos", desklink_vpx))]
31
+ #[cfg(all(any(target_os = "macos", target_os = "windows"), desklink_vpx))]
24
32
  mod encoder;
25
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
33
+ #[cfg(any(
34
+ target_os = "linux",
35
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
36
+ ))]
26
37
  mod h264;
27
38
  #[cfg(target_os = "linux")]
28
39
  mod h264_nvenc;
@@ -32,13 +43,19 @@ mod h264_openh264;
32
43
  mod h264_vaapi;
33
44
  #[cfg(all(target_os = "macos", desklink_vpx))]
34
45
  mod h264_vt;
35
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
46
+ #[cfg(any(
47
+ target_os = "linux",
48
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
49
+ ))]
36
50
  mod indicator;
37
51
  #[cfg(target_os = "linux")]
38
52
  mod input;
39
53
  #[cfg(target_os = "macos")]
40
54
  #[path = "input_mac.rs"]
41
55
  mod input;
56
+ #[cfg(target_os = "windows")]
57
+ #[path = "input_win.rs"]
58
+ mod input;
42
59
  #[cfg(target_os = "linux")]
43
60
  mod keeper;
44
61
  #[cfg(target_os = "linux")]
@@ -46,44 +63,76 @@ mod keymap;
46
63
  #[cfg(target_os = "macos")]
47
64
  #[path = "keymap_mac.rs"]
48
65
  mod keymap;
66
+ #[cfg(target_os = "windows")]
67
+ #[path = "keymap_win.rs"]
68
+ mod keymap;
49
69
  #[cfg(target_os = "macos")]
50
70
  mod mac;
51
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
71
+ #[cfg(any(
72
+ target_os = "linux",
73
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
74
+ ))]
52
75
  mod peer;
53
76
  #[cfg(target_os = "linux")]
54
77
  mod portal;
55
78
  #[cfg(target_os = "macos")]
56
79
  #[path = "portal_mac.rs"]
57
80
  mod portal;
58
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
81
+ #[cfg(target_os = "windows")]
82
+ #[path = "portal_win.rs"]
83
+ mod portal;
84
+ #[cfg(any(
85
+ target_os = "linux",
86
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
87
+ ))]
59
88
  mod protocol;
60
89
  #[cfg(target_os = "linux")]
61
90
  mod session;
62
- #[cfg(all(target_os = "macos", desklink_vpx))]
91
+ #[cfg(all(any(target_os = "macos", target_os = "windows"), desklink_vpx))]
63
92
  mod session;
93
+ #[cfg(target_os = "windows")]
94
+ mod win;
64
95
  #[cfg(target_os = "linux")]
65
96
  mod x11;
66
- #[cfg(target_os = "macos")]
97
+ #[cfg(any(target_os = "macos", target_os = "windows"))]
67
98
  #[path = "x11_mac.rs"]
68
99
  mod x11;
69
100
 
70
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
101
+ #[cfg(any(
102
+ target_os = "linux",
103
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
104
+ ))]
71
105
  use anyhow::Result;
72
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
106
+ #[cfg(any(
107
+ target_os = "linux",
108
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
109
+ ))]
73
110
  use protocol::{ErrorBody, Event, Request, Response};
74
111
  #[cfg(target_os = "linux")]
75
112
  use std::sync::mpsc as std_mpsc;
76
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
113
+ #[cfg(any(
114
+ target_os = "linux",
115
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
116
+ ))]
77
117
  use std::time::Duration;
78
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
118
+ #[cfg(any(
119
+ target_os = "linux",
120
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
121
+ ))]
79
122
  use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
80
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
123
+ #[cfg(any(
124
+ target_os = "linux",
125
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
126
+ ))]
81
127
  use tokio::sync::mpsc as tokio_mpsc;
82
128
 
83
129
  #[cfg(target_os = "linux")]
84
130
  unsafe extern "C" {
131
+ fn desklink_point_overlay_main(display: *const std::ffi::c_char) -> i32;
85
132
  fn desklink_agent_overlay_main(display: *const std::ffi::c_char) -> i32;
86
133
  fn desklink_agent_overlay_wayland(source_w: i32, source_h: i32) -> i32;
134
+ fn desklink_wayland_layer_shell_available() -> i32;
135
+ fn desklink_point_overlay_wayland(w: i32, h: i32, known: i32, x: i32, y: i32) -> i32;
87
136
  }
88
137
 
89
138
  #[cfg(target_os = "linux")]
@@ -105,11 +154,39 @@ fn main() {
105
154
  println!("{}", env!("CARGO_PKG_VERSION"));
106
155
  0
107
156
  }
157
+ "point-overlay" => {
158
+ let display = args.get(1).map(String::as_str).unwrap_or("");
159
+ let display = std::ffi::CString::new(display).expect("valid X display");
160
+ unsafe { desklink_point_overlay_main(display.as_ptr()) }
161
+ }
108
162
  "agent-overlay" => {
109
163
  let display = args.get(1).map(String::as_str).unwrap_or("");
110
164
  let display = std::ffi::CString::new(display).expect("valid X display");
111
165
  unsafe { desklink_agent_overlay_main(display.as_ptr()) }
112
166
  }
167
+ "point-layer-shell-probe" => match unsafe { desklink_wayland_layer_shell_available() } {
168
+ 1 => {
169
+ println!("READY");
170
+ 0
171
+ }
172
+ 0 => {
173
+ println!("layer_shell_unavailable");
174
+ 2
175
+ }
176
+ _ => 2,
177
+ },
178
+ "point-overlay-wayland" => {
179
+ let values: Vec<i32> = args.iter().skip(1).filter_map(|v| v.parse().ok()).collect();
180
+ if values.len() != 5 || values[0] <= 0 || values[1] <= 0 {
181
+ 2
182
+ } else {
183
+ unsafe {
184
+ desklink_point_overlay_wayland(
185
+ values[0], values[1], values[2], values[3], values[4],
186
+ )
187
+ }
188
+ }
189
+ }
113
190
  "agent-overlay-wayland" => {
114
191
  let width = args.get(1).and_then(|v| v.parse().ok()).unwrap_or(0);
115
192
  let height = args.get(2).and_then(|v| v.parse().ok()).unwrap_or(0);
@@ -149,7 +226,10 @@ fn main() {
149
226
  std::process::exit(code);
150
227
  }
151
228
 
152
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
229
+ #[cfg(any(
230
+ target_os = "linux",
231
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
232
+ ))]
153
233
  fn report(result: Result<()>) -> i32 {
154
234
  match result {
155
235
  Ok(()) => 0,
@@ -295,7 +375,7 @@ fn frame_shape(frame: &crate::convert::I420) -> u64 {
295
375
 
296
376
  #[cfg(target_os = "linux")]
297
377
  async fn probe_portal(seconds: u64) -> Result<()> {
298
- let portal = portal::open(None).await?;
378
+ let portal = portal::open(None, protocol::CursorMode::Embedded).await?;
299
379
  eprintln!("portal source: {:?}", portal.source);
300
380
  // The source's own size: a zero box converts nothing and reports no frames.
301
381
  let (width, height) = crate::convert::fit(
@@ -311,6 +391,7 @@ async fn probe_portal(seconds: u64) -> Result<()> {
311
391
  width,
312
392
  height,
313
393
  30,
394
+ None,
314
395
  Box::new(move |frame, seq, _raw| {
315
396
  let _ = tx.send((frame.width, frame.height, seq));
316
397
  }),
@@ -349,7 +430,10 @@ async fn probe_portal(seconds: u64) -> Result<()> {
349
430
  }
350
431
 
351
432
  /// The local control protocol loop.
352
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
433
+ #[cfg(any(
434
+ target_os = "linux",
435
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
436
+ ))]
353
437
  async fn serve() -> Result<()> {
354
438
  let (out_tx, mut out_rx) = tokio_mpsc::unbounded_channel::<String>();
355
439
  let writer = tokio::spawn(async move {
@@ -434,13 +518,16 @@ async fn serve() -> Result<()> {
434
518
  if let Some(session) = current.take() {
435
519
  session.close("the consumer disconnected").await;
436
520
  }
437
- let _ = out_tx.send(String::new()).ok();
521
+ drop(events_tx);
438
522
  drop(out_tx);
439
523
  let _ = writer.await;
440
524
  Ok(())
441
525
  }
442
526
 
443
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
527
+ #[cfg(any(
528
+ target_os = "linux",
529
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
530
+ ))]
444
531
  fn render_event(notice: session::Notice) -> Option<String> {
445
532
  let session::Notice { session_id, event } = notice;
446
533
  let (name, params) = match event {
@@ -471,9 +558,17 @@ fn render_event(notice: session::Notice) -> Option<String> {
471
558
  "session.state",
472
559
  serde_json::json!({ "sessionId": session_id, "capture": capture, "transport": transport, "firstFrame": first_frame }),
473
560
  ),
474
- session::SessionEvent::Frame { seq, damage } => (
561
+ session::SessionEvent::Cursor { position } => (
562
+ "session.cursor",
563
+ serde_json::json!({ "sessionId": session_id, "x": position.x, "y": position.y, "visible": position.visible, "timestamp_us": position.timestamp_us }),
564
+ ),
565
+ session::SessionEvent::Frame {
566
+ seq,
567
+ damage,
568
+ timestamp_us,
569
+ } => (
475
570
  "session.frame.changed",
476
- serde_json::json!({ "sessionId": session_id, "seq": seq, "damage": damage }),
571
+ serde_json::json!({ "sessionId": session_id, "seq": seq, "damage": damage, "timestamp_us": timestamp_us }),
477
572
  ),
478
573
  session::SessionEvent::Input { input } => (
479
574
  "session.input",
@@ -503,7 +598,10 @@ fn render_event(notice: session::Notice) -> Option<String> {
503
598
  .ok()
504
599
  }
505
600
 
506
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
601
+ #[cfg(any(
602
+ target_os = "linux",
603
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
604
+ ))]
507
605
  async fn dispatch(
508
606
  request: &Request,
509
607
  hello_seen: &mut bool,
@@ -546,6 +644,7 @@ async fn dispatch(
546
644
  .map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
547
645
  let open = session::OpenRequest {
548
646
  source: params.source,
647
+ cursor: params.cursor,
549
648
  permissions: params.permissions,
550
649
  max_width: params.max_width,
551
650
  max_height: params.max_height,
@@ -580,10 +679,21 @@ async fn dispatch(
580
679
  "origin": { "x": session.source().origin_x, "y": session.source().origin_y },
581
680
  },
582
681
  "geometry": session.geometry(),
682
+ "cursor": session.cursor_info(),
583
683
  });
584
684
  *current = Some(session);
585
685
  Ok(result)
586
686
  }
687
+ "session.point" => {
688
+ let session = require_session(current)?;
689
+ let params: protocol::PointParams = serde_json::from_value(request.params.clone())
690
+ .map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
691
+ check_session(session, &params.session_id, None)?;
692
+ session
693
+ .point(&params)
694
+ .map_err(|error| ErrorBody::new(error.code, error.message))?;
695
+ Ok(serde_json::json!({"shown": !params.clear}))
696
+ }
587
697
  "session.description" => {
588
698
  let session = require_session(current)?;
589
699
  let params: protocol::DescriptionParams =
@@ -705,7 +815,10 @@ async fn dispatch(
705
815
  }
706
816
  }
707
817
 
708
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
818
+ #[cfg(any(
819
+ target_os = "linux",
820
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
821
+ ))]
709
822
  fn require_session(
710
823
  current: &mut Option<session::Session>,
711
824
  ) -> std::result::Result<&mut session::Session, ErrorBody> {
@@ -716,7 +829,10 @@ fn require_session(
716
829
 
717
830
  /// A late message for a session that has already ended must not revive it, and a
718
831
  /// message for a different session must not reach this one.
719
- #[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
832
+ #[cfg(any(
833
+ target_os = "linux",
834
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
835
+ ))]
720
836
  fn check_session(
721
837
  session: &session::Session,
722
838
  id: &str,
@@ -741,6 +857,11 @@ fn main() {
741
857
  std::process::exit(mac::run());
742
858
  }
743
859
 
860
+ #[cfg(target_os = "windows")]
861
+ fn main() {
862
+ std::process::exit(win::run());
863
+ }
864
+
744
865
  #[cfg(all(test, target_os = "linux"))]
745
866
  mod tests {
746
867
  use super::*;
@@ -762,6 +883,27 @@ mod tests {
762
883
  assert!(!hello_seen, "a refused handshake must not open the gate");
763
884
  }
764
885
 
886
+ #[test]
887
+ fn cursor_notice_preserves_source_coordinates_visibility_and_clock() {
888
+ let line = render_event(session::Notice {
889
+ session_id: String::from("cursor-session"),
890
+ event: session::SessionEvent::Cursor {
891
+ position: protocol::CursorPosition {
892
+ x: 32,
893
+ y: 64,
894
+ visible: false,
895
+ timestamp_us: 123456,
896
+ },
897
+ },
898
+ })
899
+ .unwrap();
900
+ let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
901
+ assert_eq!(
902
+ parsed,
903
+ serde_json::json!({"event":"session.cursor","params":{"sessionId":"cursor-session","x":32,"y":64,"visible":false,"timestamp_us":123456}})
904
+ );
905
+ }
906
+
765
907
  #[tokio::test]
766
908
  async fn the_state_notification_carries_exactly_what_the_document_promises() {
767
909
  let line = render_event(session::Notice {