@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.
- package/NOTICE +3 -0
- package/README.md +63 -15
- package/bin/desklink-host.mjs +12 -4
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +34 -4
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +32 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +6 -0
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +45 -6
- package/dist/resolveEngine.js.map +1 -1
- package/docs/PROTOCOL.md +119 -6
- package/engine/Cargo.lock +2 -2
- package/engine/Cargo.toml +7 -12
- package/engine/build.rs +61 -5
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/src/capture.rs +92 -3
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/cursor.rs +118 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +28 -6
- package/engine/src/main.rs +165 -23
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +49 -0
- package/engine/src/session.rs +467 -17
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +15 -0
- package/package.json +4 -4
package/engine/src/portal.rs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
+
}
|
package/engine/src/protocol.rs
CHANGED
|
@@ -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
|
+
}
|