@desklink/host 0.1.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.
Files changed (142) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +44 -0
  3. package/README.md +266 -0
  4. package/bin/desklink-host.mjs +113 -0
  5. package/dist/bridge.d.ts +57 -0
  6. package/dist/bridge.d.ts.map +1 -0
  7. package/dist/bridge.js +208 -0
  8. package/dist/bridge.js.map +1 -0
  9. package/dist/engineProcess.d.ts +82 -0
  10. package/dist/engineProcess.d.ts.map +1 -0
  11. package/dist/engineProcess.js +243 -0
  12. package/dist/engineProcess.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +12 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/protocol.d.ts +169 -0
  18. package/dist/protocol.d.ts.map +1 -0
  19. package/dist/protocol.js +18 -0
  20. package/dist/protocol.js.map +1 -0
  21. package/dist/resolveEngine.d.ts +34 -0
  22. package/dist/resolveEngine.d.ts.map +1 -0
  23. package/dist/resolveEngine.js +103 -0
  24. package/dist/resolveEngine.js.map +1 -0
  25. package/docs/BOUNDARY.md +145 -0
  26. package/docs/PROTOCOL.md +363 -0
  27. package/engine/Cargo.lock +3077 -0
  28. package/engine/Cargo.toml +37 -0
  29. package/engine/build.rs +107 -0
  30. package/engine/native/inputtino_shim.cpp +11 -0
  31. package/engine/native/vpx_shim.c +184 -0
  32. package/engine/native/wrapper.h +7 -0
  33. package/engine/src/capture.rs +510 -0
  34. package/engine/src/clipboard.rs +132 -0
  35. package/engine/src/convert.rs +299 -0
  36. package/engine/src/encoder.rs +204 -0
  37. package/engine/src/input.rs +355 -0
  38. package/engine/src/keymap.rs +338 -0
  39. package/engine/src/main.rs +614 -0
  40. package/engine/src/peer.rs +775 -0
  41. package/engine/src/portal.rs +115 -0
  42. package/engine/src/protocol.rs +331 -0
  43. package/engine/src/session.rs +2159 -0
  44. package/engine/src/x11.rs +244 -0
  45. package/engine/vendor/inputtino/.clang-format +166 -0
  46. package/engine/vendor/inputtino/.clang-tidy +30 -0
  47. package/engine/vendor/inputtino/.dockerignore +11 -0
  48. package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
  49. package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
  50. package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
  51. package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
  52. package/engine/vendor/inputtino/CMakeLists.txt +187 -0
  53. package/engine/vendor/inputtino/LICENSE +21 -0
  54. package/engine/vendor/inputtino/README.md +110 -0
  55. package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
  56. package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
  57. package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
  58. package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
  59. package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
  60. package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
  61. package/engine/vendor/inputtino/bindings/python/README.md +215 -0
  62. package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
  63. package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
  64. package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
  65. package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
  66. package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
  67. package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
  68. package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
  69. package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
  70. package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
  71. package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
  72. package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
  73. package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
  74. package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
  75. package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
  76. package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
  77. package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
  78. package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
  79. package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
  80. package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
  81. package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
  82. package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
  83. package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
  84. package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
  85. package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
  86. package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
  87. package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
  88. package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
  89. package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
  90. package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
  91. package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
  92. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
  93. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
  94. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
  95. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
  96. package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
  97. package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
  98. package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
  99. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
  100. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
  101. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
  102. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
  103. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
  104. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
  105. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
  106. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
  107. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
  108. package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
  109. package/engine/vendor/inputtino/docker/Dockerfile +70 -0
  110. package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
  111. package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
  112. package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
  113. package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
  114. package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
  115. package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
  116. package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
  117. package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
  118. package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
  119. package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
  120. package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
  121. package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
  122. package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
  123. package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
  124. package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
  125. package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
  126. package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
  127. package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
  128. package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
  129. package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
  130. package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
  131. package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
  132. package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
  133. package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
  134. package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
  135. package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
  136. package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
  137. package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
  138. package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
  139. package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
  140. package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
  141. package/examples/reference-client.html +428 -0
  142. package/package.json +38 -0
@@ -0,0 +1,115 @@
1
+ //! Portal (XDG Desktop Portal ScreenCast) session negotiation.
2
+ //!
3
+ //! The portal is the only capture path this engine asks the compositor for: it
4
+ //! is what carries the user's consent, works across Wayland compositors, and
5
+ //! hands back a PipeWire remote that [`crate::capture`] consumes. No
6
+ //! compositor-private protocol is used.
7
+
8
+ use anyhow::{Context, Result};
9
+ use ashpd::desktop::screencast::{CursorMode, Screencast, SourceType};
10
+ use ashpd::desktop::PersistMode;
11
+ use ashpd::enumflags2::BitFlags;
12
+ use std::os::fd::OwnedFd;
13
+
14
+ /// What the portal gave us: a PipeWire remote plus the identity of the selected
15
+ /// source. `width`/`height` are the source's logical size in the compositor's
16
+ /// own scale; `position` is its origin in the desktop layout.
17
+ #[derive(Debug, Clone, serde::Serialize)]
18
+ pub struct SelectedSource {
19
+ pub node_id: u32,
20
+ pub width: i32,
21
+ pub height: i32,
22
+ #[serde(skip_serializing_if = "Option::is_none")]
23
+ pub position: Option<(i32, i32)>,
24
+ #[serde(skip_serializing_if = "Option::is_none")]
25
+ pub source_type: Option<String>,
26
+ /// Absolute origin of the selected source in the desktop layout. Reported
27
+ /// for diagnostics and for geometry consumers only: input is applied in the
28
+ /// source's own pixels, and nothing here is added to a converted
29
+ /// coordinate.
30
+ pub origin_x: i32,
31
+ pub origin_y: i32,
32
+ }
33
+
34
+ pub struct PortalSession {
35
+ pub fd: OwnedFd,
36
+ pub source: SelectedSource,
37
+ pub restore_token: Option<String>,
38
+ }
39
+
40
+ /// Ask the portal for a screen cast session.
41
+ ///
42
+ /// `restore_token` is a token from an earlier session; supplying it lets a
43
+ /// backend skip the picker when its policy allows it. The returned token must be
44
+ /// persisted by the consumer and is single-use.
45
+ pub async fn open(restore_token: Option<&str>) -> Result<PortalSession> {
46
+ let proxy = Screencast::new()
47
+ .await
48
+ .context("compositor has no ScreenCast portal")?;
49
+
50
+ if proxy
51
+ .available_source_types()
52
+ .await
53
+ .is_ok_and(|types| !types.contains(SourceType::Monitor))
54
+ {
55
+ anyhow::bail!("ScreenCast portal advertises no monitor source");
56
+ }
57
+
58
+ let session = proxy
59
+ .create_session()
60
+ .await
61
+ .context("ScreenCast.CreateSession failed")?;
62
+
63
+ proxy
64
+ .select_sources(
65
+ &session,
66
+ // The cursor is part of the picture the user drives; hiding it would
67
+ // make precise placement impossible.
68
+ CursorMode::Embedded,
69
+ BitFlags::from(SourceType::Monitor),
70
+ false,
71
+ restore_token,
72
+ // Request a durable grant, not one tied to this engine process.
73
+ // The portal still owns consent and may decline persistence.
74
+ PersistMode::ExplicitlyRevoked,
75
+ )
76
+ .await
77
+ .context("ScreenCast.SelectSources failed")?
78
+ .response()
79
+ .context("ScreenCast.SelectSources was cancelled")?;
80
+
81
+ // `Start` is what the compositor uses to present its consent UI.
82
+ let streams = proxy
83
+ .start(&session, None)
84
+ .await
85
+ .context("ScreenCast.Start failed")?
86
+ .response()
87
+ .context("screen capture was not granted")?;
88
+
89
+ let stream = streams
90
+ .streams()
91
+ .first()
92
+ .context("portal returned no capture stream")?;
93
+
94
+ let (width, height) = stream.size().context("portal stream has no logical size")?;
95
+ let position = stream.position();
96
+
97
+ let fd = proxy
98
+ .open_pipe_wire_remote(&session)
99
+ .await
100
+ .context("OpenPipeWireRemote failed")?;
101
+
102
+ Ok(PortalSession {
103
+ fd,
104
+ source: SelectedSource {
105
+ node_id: stream.pipe_wire_node_id(),
106
+ width,
107
+ height,
108
+ position,
109
+ source_type: stream.source_type().map(|t| format!("{t:?}")),
110
+ origin_x: position.map(|(x, _)| x).unwrap_or(0),
111
+ origin_y: position.map(|(_, y)| y).unwrap_or(0),
112
+ },
113
+ restore_token: streams.restore_token().map(str::to_owned),
114
+ })
115
+ }
@@ -0,0 +1,331 @@
1
+ //! Wire types for the local control protocol and for the session's control
2
+ //! channel. Documented in `docs/PROTOCOL.md`; this module is the definition.
3
+
4
+ use serde::{Deserialize, Serialize};
5
+
6
+ pub const PROTOCOL_VERSION: u32 = 2;
7
+
8
+ /// A request from the consumer to the engine.
9
+ #[derive(Debug, Deserialize)]
10
+ pub struct Request {
11
+ #[serde(default)]
12
+ pub id: Option<u64>,
13
+ pub method: String,
14
+ #[serde(default)]
15
+ pub params: serde_json::Value,
16
+ }
17
+
18
+ /// A reply to one request. Exactly one of `result`/`error` is set.
19
+ #[derive(Debug, Serialize)]
20
+ pub struct Response {
21
+ pub id: u64,
22
+ #[serde(skip_serializing_if = "Option::is_none")]
23
+ pub result: Option<serde_json::Value>,
24
+ #[serde(skip_serializing_if = "Option::is_none")]
25
+ pub error: Option<ErrorBody>,
26
+ }
27
+
28
+ #[derive(Debug, Serialize)]
29
+ pub struct ErrorBody {
30
+ pub code: String,
31
+ pub message: String,
32
+ }
33
+
34
+ impl ErrorBody {
35
+ pub fn new(code: &str, message: impl Into<String>) -> Self {
36
+ Self {
37
+ code: code.to_owned(),
38
+ message: message.into(),
39
+ }
40
+ }
41
+ }
42
+
43
+ /// A notification the engine sends on its own.
44
+ #[derive(Debug, Serialize)]
45
+ pub struct Event {
46
+ pub event: String,
47
+ pub params: serde_json::Value,
48
+ }
49
+
50
+ #[derive(Debug, Default, Deserialize)]
51
+ pub struct HelloParams {
52
+ #[serde(default)]
53
+ pub protocol: u32,
54
+ }
55
+
56
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57
+ #[serde(rename_all = "snake_case")]
58
+ pub enum Permission {
59
+ View,
60
+ Control,
61
+ Clipboard,
62
+ }
63
+
64
+ #[derive(Debug, Clone, Deserialize)]
65
+ pub struct IceServerParam {
66
+ #[serde(default)]
67
+ pub urls: Vec<String>,
68
+ #[serde(default)]
69
+ pub username: Option<String>,
70
+ #[serde(default)]
71
+ pub credential: Option<String>,
72
+ }
73
+
74
+ /// Which desktop to capture.
75
+ ///
76
+ /// The portal is the default because on a Wayland desktop it is what carries the
77
+ /// user's consent. An explicit X display is the other supported backend: a
78
+ /// machine with no working screen-cast portal, a headless X server, or a remote
79
+ /// X session. Asking for one is a deployment choice, not a test switch.
80
+ #[derive(Debug, Clone, Deserialize)]
81
+ #[serde(tag = "kind", rename_all = "snake_case")]
82
+ pub enum SourceRequest {
83
+ Portal,
84
+ X11 {
85
+ #[serde(default)]
86
+ display: Option<String>,
87
+ },
88
+ }
89
+
90
+ #[derive(Debug, Deserialize)]
91
+ pub struct OpenParams {
92
+ /// Absent means "the portal, with the user's consent".
93
+ #[serde(default)]
94
+ pub source: Option<SourceRequest>,
95
+ #[serde(default)]
96
+ pub permissions: Vec<Permission>,
97
+ #[serde(default = "default_max_width")]
98
+ pub max_width: usize,
99
+ #[serde(default = "default_max_height")]
100
+ pub max_height: usize,
101
+ #[serde(default = "default_bitrate")]
102
+ pub bitrate_kbps: u32,
103
+ #[serde(default = "default_fps")]
104
+ pub max_fps: u32,
105
+ #[serde(default)]
106
+ pub ice_servers: Vec<IceServerParam>,
107
+ #[serde(default)]
108
+ pub restore_token: Option<String>,
109
+ #[serde(default)]
110
+ pub ttl_seconds: Option<u64>,
111
+ /// Also listen for ICE over TCP on this computer's loopback, for a client
112
+ /// whose only way here is a local forward (an SSH tunnel).
113
+ #[serde(default)]
114
+ pub loopback_tcp: bool,
115
+ }
116
+
117
+ /// The encode box when the consumer names none: a desktop's own pixels up to
118
+ /// 4K, because a phone zooms into the picture and text has to survive that.
119
+ fn default_max_width() -> usize {
120
+ 3840
121
+ }
122
+ fn default_max_height() -> usize {
123
+ 2160
124
+ }
125
+ /// Zero asks the engine to size the rate to the encoded surface.
126
+ fn default_bitrate() -> u32 {
127
+ 0
128
+ }
129
+ fn default_fps() -> u32 {
130
+ 30
131
+ }
132
+
133
+ #[derive(Debug, Deserialize)]
134
+ pub struct SessionRef {
135
+ pub session_id: String,
136
+ #[serde(default)]
137
+ pub generation: Option<u64>,
138
+ }
139
+
140
+ #[derive(Debug, Deserialize)]
141
+ pub struct DescriptionParams {
142
+ pub session_id: String,
143
+ #[serde(default)]
144
+ pub generation: Option<u64>,
145
+ pub description: RtcDescription,
146
+ }
147
+
148
+ #[derive(Debug, Deserialize)]
149
+ pub struct RtcDescription {
150
+ #[serde(rename = "type")]
151
+ pub kind: String,
152
+ pub sdp: String,
153
+ }
154
+
155
+ #[derive(Debug, Deserialize)]
156
+ pub struct CandidateParams {
157
+ pub session_id: String,
158
+ #[serde(default)]
159
+ pub generation: Option<u64>,
160
+ pub candidate: String,
161
+ #[serde(default)]
162
+ pub sdp_mid: Option<String>,
163
+ #[serde(default, rename = "sdpMLineIndex", alias = "sdp_m_line_index")]
164
+ pub sdp_m_line_index: Option<u16>,
165
+ }
166
+
167
+ #[derive(Debug, Deserialize)]
168
+ pub struct ClipboardParams {
169
+ pub session_id: String,
170
+ #[serde(default)]
171
+ pub mime: Option<String>,
172
+ #[serde(default)]
173
+ pub text: Option<String>,
174
+ }
175
+
176
+ /// Messages the client sends on the session's control channel.
177
+ ///
178
+ /// `seq` is the client's own ordering. The engine refuses a repeat or a lower
179
+ /// value rather than re-applying an action, so a replayed frame cannot move the
180
+ /// pointer twice.
181
+ #[derive(Debug, Deserialize)]
182
+ #[serde(tag = "kind", rename_all = "snake_case")]
183
+ pub enum ControlMessage {
184
+ Pointer {
185
+ phase: PointerPhase,
186
+ x: i64,
187
+ y: i64,
188
+ #[serde(default = "left_button")]
189
+ button: i64,
190
+ #[serde(default)]
191
+ seq: u64,
192
+ },
193
+ /// Detents; fractions scroll smoothly where the desktop supports it.
194
+ Wheel {
195
+ #[serde(default)]
196
+ dx: f64,
197
+ #[serde(default)]
198
+ dy: f64,
199
+ #[serde(default)]
200
+ seq: u64,
201
+ },
202
+ Key {
203
+ #[serde(default)]
204
+ name: Option<String>,
205
+ #[serde(default)]
206
+ character: Option<String>,
207
+ down: bool,
208
+ #[serde(default)]
209
+ modifiers: Vec<String>,
210
+ #[serde(default)]
211
+ seq: u64,
212
+ },
213
+ Text {
214
+ text: String,
215
+ #[serde(default)]
216
+ seq: u64,
217
+ },
218
+ ReleaseAll {
219
+ #[serde(default)]
220
+ seq: u64,
221
+ },
222
+ ClipboardRead {
223
+ request: String,
224
+ #[serde(default)]
225
+ seq: u64,
226
+ },
227
+ ClipboardWrite {
228
+ request: String,
229
+ text: String,
230
+ #[serde(default)]
231
+ seq: u64,
232
+ },
233
+ }
234
+
235
+ fn left_button() -> i64 {
236
+ 1
237
+ }
238
+
239
+ impl ControlMessage {
240
+ pub fn seq(&self) -> u64 {
241
+ match self {
242
+ Self::Pointer { seq, .. }
243
+ | Self::Wheel { seq, .. }
244
+ | Self::Key { seq, .. }
245
+ | Self::Text { seq, .. }
246
+ | Self::ReleaseAll { seq }
247
+ | Self::ClipboardRead { seq, .. }
248
+ | Self::ClipboardWrite { seq, .. } => *seq,
249
+ }
250
+ }
251
+ }
252
+
253
+ #[derive(Debug, Clone, Copy, Deserialize)]
254
+ #[serde(rename_all = "snake_case")]
255
+ pub enum PointerPhase {
256
+ Move,
257
+ Down,
258
+ Up,
259
+ Cancel,
260
+ }
261
+
262
+ /// Messages the engine sends back on the control channel.
263
+ #[derive(Debug, Serialize)]
264
+ #[serde(tag = "kind", rename_all = "snake_case")]
265
+ pub enum ControlReply<'a> {
266
+ Hello {
267
+ protocol: u32,
268
+ geometry: serde_json::Value,
269
+ },
270
+ Ack {
271
+ seq: u64,
272
+ },
273
+ Rejected {
274
+ seq: u64,
275
+ code: &'a str,
276
+ message: &'a str,
277
+ },
278
+ Clipboard {
279
+ request: &'a str,
280
+ text: String,
281
+ truncated: bool,
282
+ #[serde(skip_serializing_if = "Option::is_none")]
283
+ error: Option<&'a str>,
284
+ },
285
+ Revoked {
286
+ reason: &'a str,
287
+ },
288
+ }
289
+
290
+ #[cfg(test)]
291
+ mod tests {
292
+ use super::*;
293
+
294
+ #[test]
295
+ fn a_pointer_message_round_trips_with_its_sequence() {
296
+ let parsed: ControlMessage =
297
+ serde_json::from_str(r#"{"kind":"pointer","phase":"down","x":10,"y":20,"seq":3}"#)
298
+ .unwrap();
299
+ match parsed {
300
+ ControlMessage::Pointer {
301
+ phase,
302
+ x,
303
+ y,
304
+ seq,
305
+ button,
306
+ } => {
307
+ assert!(matches!(phase, PointerPhase::Down));
308
+ assert_eq!((x, y, seq, button), (10, 20, 3, 1));
309
+ }
310
+ other => panic!("wrong variant: {other:?}"),
311
+ }
312
+ }
313
+
314
+ #[test]
315
+ fn an_unknown_action_is_refused_rather_than_ignored() {
316
+ assert!(serde_json::from_str::<ControlMessage>(r#"{"kind":"teleport"}"#).is_err());
317
+ }
318
+
319
+ #[test]
320
+ fn open_defaults_keep_the_desktops_own_pixels_up_to_4k() {
321
+ let params: OpenParams =
322
+ serde_json::from_str(r#"{"permissions":["view","control"]}"#).unwrap();
323
+ assert_eq!(params.max_width, 3840);
324
+ assert_eq!(params.max_height, 2160);
325
+ assert_eq!(
326
+ params.bitrate_kbps, 0,
327
+ "the engine sizes the rate to the surface"
328
+ );
329
+ assert_eq!(params.max_fps, 30);
330
+ }
331
+ }