@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.
@@ -33,6 +33,7 @@ pub struct SelectedSource {
33
33
 
34
34
  pub struct PortalSession {
35
35
  pub fd: OwnedFd,
36
+ pub cursor_mode: &'static str,
36
37
  pub source: SelectedSource,
37
38
  pub restore_token: Option<String>,
38
39
  }
@@ -46,7 +47,10 @@ pub async fn open_display(_: Option<u32>) -> Result<PortalSession> {
46
47
  anyhow::bail!("display selection is only supported on macOS")
47
48
  }
48
49
 
49
- pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
50
+ pub async fn open(
51
+ restore_token: Option<&str>,
52
+ cursor: crate::protocol::CursorMode,
53
+ ) -> Result<PortalSession> {
50
54
  let proxy = Screencast::new()
51
55
  .await
52
56
  .context("compositor has no ScreenCast portal")?;
@@ -59,6 +63,15 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
59
63
  anyhow::bail!("ScreenCast portal advertises no monitor source");
60
64
  }
61
65
 
66
+ let cursor_mode = if cursor == crate::protocol::CursorMode::Hidden {
67
+ let available = proxy
68
+ .available_cursor_modes()
69
+ .await
70
+ .context("portal did not advertise cursor modes")?;
71
+ choose_cursor_mode(available)?
72
+ } else {
73
+ CursorMode::Embedded
74
+ };
62
75
  let session = proxy
63
76
  .create_session()
64
77
  .await
@@ -67,9 +80,7 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
67
80
  proxy
68
81
  .select_sources(
69
82
  &session,
70
- // The cursor is part of the picture the user drives; hiding it would
71
- // make precise placement impossible.
72
- CursorMode::Embedded,
83
+ cursor_mode,
73
84
  BitFlags::from(SourceType::Monitor),
74
85
  false,
75
86
  restore_token,
@@ -104,6 +115,11 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
104
115
  .context("OpenPipeWireRemote failed")?;
105
116
 
106
117
  Ok(PortalSession {
118
+ cursor_mode: match cursor_mode {
119
+ CursorMode::Metadata => "metadata",
120
+ CursorMode::Hidden => "hidden",
121
+ _ => "embedded",
122
+ },
107
123
  fd,
108
124
  source: SelectedSource {
109
125
  node_id: stream.pipe_wire_node_id(),
@@ -117,3 +133,25 @@ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
117
133
  restore_token: streams.restore_token().map(str::to_owned),
118
134
  })
119
135
  }
136
+
137
+ fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
138
+ if available.contains(CursorMode::Metadata) {
139
+ return Ok(CursorMode::Metadata);
140
+ }
141
+ anyhow::bail!("cursor_positions_unavailable: portal does not provide cursor metadata")
142
+ }
143
+
144
+ #[cfg(test)]
145
+ mod tests {
146
+ use super::*;
147
+ #[test]
148
+ fn hidden_requires_metadata_and_refuses_hidden_only_portals() {
149
+ assert_eq!(
150
+ choose_cursor_mode(CursorMode::Metadata | CursorMode::Hidden).unwrap(),
151
+ CursorMode::Metadata
152
+ );
153
+ let error = choose_cursor_mode(CursorMode::Hidden.into()).unwrap_err();
154
+ assert!(error.to_string().contains("cursor_positions_unavailable:"));
155
+ assert!(choose_cursor_mode(CursorMode::Embedded.into()).is_err());
156
+ }
157
+ }
@@ -0,0 +1,37 @@
1
+ use anyhow::{bail, Result};
2
+ use serde_json::Value;
3
+
4
+ /// A selected display; Windows enumeration is deferred to the capture lane.
5
+ #[derive(Debug, Clone, serde::Serialize)]
6
+ pub struct SelectedSource {
7
+ pub node_id: u32,
8
+ pub width: i32,
9
+ pub height: i32,
10
+ #[serde(skip_serializing_if = "Option::is_none")]
11
+ pub position: Option<(i32, i32)>,
12
+ #[serde(skip_serializing_if = "Option::is_none")]
13
+ pub source_type: Option<String>,
14
+ pub origin_x: i32,
15
+ pub origin_y: i32,
16
+ }
17
+
18
+ pub struct PortalSession {
19
+ pub source: SelectedSource,
20
+ pub restore_token: Option<String>,
21
+ }
22
+
23
+ pub async fn open(_restore_token: Option<&str>) -> Result<PortalSession> {
24
+ open_display_inner(None)
25
+ }
26
+
27
+ pub async fn open_display(display_id: Option<u32>) -> Result<PortalSession> {
28
+ open_display_inner(display_id)
29
+ }
30
+
31
+ fn open_display_inner(_display_id: Option<u32>) -> Result<PortalSession> {
32
+ bail!("{}", crate::capture::unavailable_reason())
33
+ }
34
+
35
+ pub fn displays() -> Vec<Value> {
36
+ Vec::new()
37
+ }
@@ -111,8 +111,18 @@ pub enum EncodedCodec {
111
111
  H264,
112
112
  }
113
113
 
114
+ #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115
+ #[serde(rename_all = "snake_case")]
116
+ pub enum CursorMode {
117
+ #[default]
118
+ Embedded,
119
+ Hidden,
120
+ }
121
+
114
122
  #[derive(Debug, Deserialize)]
115
123
  pub struct OpenParams {
124
+ #[serde(default)]
125
+ pub cursor: CursorMode,
116
126
  /// Absent means "the portal, with the user's consent".
117
127
  #[serde(default)]
118
128
  pub source: Option<SourceRequest>,
@@ -168,6 +178,20 @@ fn default_fps() -> u32 {
168
178
  60
169
179
  }
170
180
 
181
+ /// A host-only visual cue; never an input message.
182
+ #[derive(Debug, Deserialize)]
183
+ #[serde(deny_unknown_fields)]
184
+ pub struct PointParams {
185
+ pub session_id: String,
186
+ pub x: Option<i64>,
187
+ pub y: Option<i64>,
188
+ #[serde(default)]
189
+ pub label: String,
190
+ pub timeout_ms: Option<u64>,
191
+ #[serde(default)]
192
+ pub clear: bool,
193
+ }
194
+
171
195
  #[derive(Debug, Deserialize)]
172
196
  pub struct FrameParams {
173
197
  pub session_id: String,
@@ -385,6 +409,16 @@ pub enum ControlReply<'a> {
385
409
  mod tests {
386
410
  use super::*;
387
411
 
412
+ #[test]
413
+ fn cursor_option_defaults_and_refuses_unknown_modes() {
414
+ let default: OpenParams = serde_json::from_str("{}").unwrap();
415
+ assert_eq!(default.cursor, CursorMode::Embedded);
416
+ let hidden: OpenParams = serde_json::from_str(r#"{"cursor":"hidden"}"#).unwrap();
417
+ assert_eq!(hidden.cursor, CursorMode::Hidden);
418
+ assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":"metadata"}"#).is_err());
419
+ assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":false}"#).is_err());
420
+ }
421
+
388
422
  #[test]
389
423
  fn a_pointer_message_round_trips_with_its_sequence() {
390
424
  let parsed: ControlMessage =
@@ -525,3 +559,18 @@ mod tests {
525
559
  assert!(agent.agent_indicator);
526
560
  }
527
561
  }
562
+
563
+ /// Monotonic engine-process clock shared by local frame and cursor notices.
564
+ pub fn timestamp_us(at: std::time::Instant) -> u64 {
565
+ static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
566
+ at.saturating_duration_since(*EPOCH.get_or_init(|| at))
567
+ .as_micros() as u64
568
+ }
569
+
570
+ #[derive(Debug, Clone, Copy)]
571
+ pub struct CursorPosition {
572
+ pub x: i32,
573
+ pub y: i32,
574
+ pub visible: bool,
575
+ pub timestamp_us: u64,
576
+ }