@desklink/host 0.5.15 → 0.5.16
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/docs/PROTOCOL.md +3 -1
- package/engine/Cargo.lock +1 -1
- package/engine/Cargo.toml +1 -1
- package/engine/src/capture.rs +0 -79
- package/engine/src/encoder.rs +0 -48
- package/engine/src/h264_nvenc.rs +0 -165
- package/engine/src/h264_openh264.rs +0 -245
- package/engine/src/h264_vaapi.rs +0 -97
- package/engine/src/indicator.rs +0 -101
- package/engine/src/input_portal.rs +337 -0
- package/engine/src/main.rs +2 -88
- package/engine/src/observe.rs +0 -33
- package/engine/src/peer.rs +0 -433
- package/engine/src/portal.rs +0 -75
- package/engine/src/session.rs +52 -1063
- package/package.json +3 -3
package/engine/src/indicator.rs
CHANGED
|
@@ -349,104 +349,3 @@ impl Drop for Indicator {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
|
-
|
|
353
|
-
#[cfg(all(test, target_os = "linux"))]
|
|
354
|
-
mod tests {
|
|
355
|
-
#[test]
|
|
356
|
-
fn cue_feed_keeps_recent_states_until_the_helper_exits() {
|
|
357
|
-
use std::os::fd::AsRawFd;
|
|
358
|
-
let mut child = std::process::Command::new("sh")
|
|
359
|
-
.args([
|
|
360
|
-
"-c",
|
|
361
|
-
"for x in 1 2 3; do echo \"P $x 0 110 1 1 -1 -1 128 99\"; done; read _",
|
|
362
|
-
])
|
|
363
|
-
.stdin(std::process::Stdio::piped())
|
|
364
|
-
.stdout(std::process::Stdio::piped())
|
|
365
|
-
.spawn()
|
|
366
|
-
.unwrap();
|
|
367
|
-
let reader = super::BufReader::new(child.stdout.take().unwrap());
|
|
368
|
-
let fd = reader.get_ref().as_raw_fd();
|
|
369
|
-
unsafe {
|
|
370
|
-
libc::fcntl(
|
|
371
|
-
fd,
|
|
372
|
-
libc::F_SETFL,
|
|
373
|
-
libc::fcntl(fd, libc::F_GETFL) | libc::O_NONBLOCK,
|
|
374
|
-
)
|
|
375
|
-
};
|
|
376
|
-
let mut feed = super::CueFeed {
|
|
377
|
-
reader: Some(reader),
|
|
378
|
-
..Default::default()
|
|
379
|
-
};
|
|
380
|
-
let mut drained = vec![];
|
|
381
|
-
for _ in 0..200 {
|
|
382
|
-
// Each drain re-reports the last two remembered states, so the
|
|
383
|
-
// same state can arrive in several drains when the helper's lines
|
|
384
|
-
// are split across reads. Keep each state once, in pipe order.
|
|
385
|
-
for cue in feed.drain() {
|
|
386
|
-
if !drained.contains(&cue.x) {
|
|
387
|
-
drained.push(cue.x);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if drained.len() >= 3 {
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
394
|
-
}
|
|
395
|
-
assert_eq!(
|
|
396
|
-
drained,
|
|
397
|
-
[1, 2, 3],
|
|
398
|
-
"a drain that finds nothing does not block"
|
|
399
|
-
);
|
|
400
|
-
let xs =
|
|
401
|
-
|feed: &mut super::CueFeed| feed.drain().iter().map(|cue| cue.x).collect::<Vec<_>>();
|
|
402
|
-
assert_eq!(
|
|
403
|
-
xs(&mut feed),
|
|
404
|
-
[2, 3],
|
|
405
|
-
"either of the last two may be on screen"
|
|
406
|
-
);
|
|
407
|
-
drop(child.stdin.take());
|
|
408
|
-
child.wait().unwrap();
|
|
409
|
-
assert_eq!(xs(&mut feed), [2, 3]);
|
|
410
|
-
assert!(
|
|
411
|
-
xs(&mut feed).is_empty(),
|
|
412
|
-
"a closed helper has no window left"
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
unsafe extern "C" {
|
|
417
|
-
fn desklink_wayland_geometry(
|
|
418
|
-
source_w: i32,
|
|
419
|
-
source_h: i32,
|
|
420
|
-
surface_w: i32,
|
|
421
|
-
surface_h: i32,
|
|
422
|
-
px: f64,
|
|
423
|
-
py: f64,
|
|
424
|
-
x: *mut f64,
|
|
425
|
-
y: *mut f64,
|
|
426
|
-
) -> i32;
|
|
427
|
-
fn desklink_wayland_policy(
|
|
428
|
-
anchor: *mut u32,
|
|
429
|
-
keyboard_focus: *mut u32,
|
|
430
|
-
empty_input: *mut u32,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
#[test]
|
|
435
|
-
fn layer_shell_maps_source_pixels_and_never_takes_input_or_focus() {
|
|
436
|
-
let (mut x, mut y) = (0.0, 0.0);
|
|
437
|
-
unsafe {
|
|
438
|
-
assert_eq!(
|
|
439
|
-
desklink_wayland_geometry(3840, 2160, 1920, 1080, 1000.0, 600.0, &mut x, &mut y),
|
|
440
|
-
1
|
|
441
|
-
);
|
|
442
|
-
assert_eq!((x, y), (500.0, 300.0));
|
|
443
|
-
assert_eq!(
|
|
444
|
-
desklink_wayland_geometry(0, 2160, 1920, 1080, 10.0, 10.0, &mut x, &mut y),
|
|
445
|
-
0
|
|
446
|
-
);
|
|
447
|
-
let (mut anchor, mut keyboard, mut empty) = (0, 1, 0);
|
|
448
|
-
desklink_wayland_policy(&mut anchor, &mut keyboard, &mut empty);
|
|
449
|
-
assert_eq!((anchor, keyboard, empty), (15, 0, 1));
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
//! Keyboard through the session portal's RemoteDesktop interface.
|
|
2
|
+
//!
|
|
3
|
+
//! Chosen only when the portal itself advertises a keyboard device, so no
|
|
4
|
+
//! host-wide `uinput` device is opened. The portal session is the engine's own:
|
|
5
|
+
//! it carries only the keycodes this session sends, it listens to nothing (this
|
|
6
|
+
//! is the injection interface, not InputCapture, which would read the host
|
|
7
|
+
//! seat), and it is closed when the adapter is dropped with the session.
|
|
8
|
+
|
|
9
|
+
use anyhow::{Context, Result};
|
|
10
|
+
use ashpd::desktop::remote_desktop::{DeviceType, KeyState, RemoteDesktop};
|
|
11
|
+
use ashpd::desktop::PersistMode;
|
|
12
|
+
use tokio::sync::mpsc;
|
|
13
|
+
|
|
14
|
+
pub struct PortalKeys(mpsc::UnboundedSender<(i32, KeyState)>);
|
|
15
|
+
|
|
16
|
+
/// Whether the portal offers a keyboard. Reads one property; creates no
|
|
17
|
+
/// session and cannot prompt.
|
|
18
|
+
pub async fn advertised() -> bool {
|
|
19
|
+
match RemoteDesktop::new().await {
|
|
20
|
+
Ok(proxy) => proxy
|
|
21
|
+
.available_device_types()
|
|
22
|
+
.await
|
|
23
|
+
.is_ok_and(|types| types.contains(DeviceType::Keyboard)),
|
|
24
|
+
Err(_) => false,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// [`advertised`] for the synchronous capabilities report. Inside the serving
|
|
29
|
+
/// runtime it runs there, so ashpd's cached bus connection lives on that
|
|
30
|
+
/// runtime; the one-shot CLI has none and builds a throwaway. A silent bus
|
|
31
|
+
/// counts as no keyboard.
|
|
32
|
+
pub fn advertised_blocking() -> bool {
|
|
33
|
+
let probe = async {
|
|
34
|
+
tokio::time::timeout(std::time::Duration::from_secs(2), advertised())
|
|
35
|
+
.await
|
|
36
|
+
.unwrap_or(false)
|
|
37
|
+
};
|
|
38
|
+
match tokio::runtime::Handle::try_current() {
|
|
39
|
+
Ok(handle) => tokio::task::block_in_place(|| handle.block_on(probe)),
|
|
40
|
+
Err(_) => tokio::runtime::Builder::new_current_thread()
|
|
41
|
+
.enable_all()
|
|
42
|
+
.build()
|
|
43
|
+
.is_ok_and(|runtime| runtime.block_on(probe)),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Open a keyboard-only RemoteDesktop session. Keys are applied in send order
|
|
48
|
+
/// by one task, which closes the portal session when the adapter is dropped.
|
|
49
|
+
pub async fn open() -> Result<PortalKeys> {
|
|
50
|
+
anyhow::ensure!(advertised().await, "the portal offers no RemoteDesktop keyboard");
|
|
51
|
+
let proxy = RemoteDesktop::new().await?;
|
|
52
|
+
let session = proxy
|
|
53
|
+
.create_session()
|
|
54
|
+
.await
|
|
55
|
+
.context("RemoteDesktop.CreateSession failed")?;
|
|
56
|
+
proxy
|
|
57
|
+
.select_devices(&session, DeviceType::Keyboard.into(), None, PersistMode::DoNot)
|
|
58
|
+
.await
|
|
59
|
+
.context("RemoteDesktop.SelectDevices failed")?
|
|
60
|
+
.response()
|
|
61
|
+
.context("RemoteDesktop.SelectDevices was cancelled")?;
|
|
62
|
+
let granted = proxy
|
|
63
|
+
.start(&session, None)
|
|
64
|
+
.await
|
|
65
|
+
.context("RemoteDesktop.Start failed")?
|
|
66
|
+
.response()
|
|
67
|
+
.context("keyboard access was not granted")?;
|
|
68
|
+
anyhow::ensure!(
|
|
69
|
+
granted.devices().contains(DeviceType::Keyboard),
|
|
70
|
+
"the portal did not grant the keyboard"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
let (keys, mut queue) = mpsc::unbounded_channel::<(i32, KeyState)>();
|
|
74
|
+
tokio::spawn(async move {
|
|
75
|
+
while let Some((code, state)) = queue.recv().await {
|
|
76
|
+
if let Err(error) = proxy.notify_keyboard_keycode(&session, code, state).await {
|
|
77
|
+
log::warn!("portal keyboard: key {code} refused: {error}");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let _ = session.close().await;
|
|
81
|
+
});
|
|
82
|
+
Ok(PortalKeys(keys))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
impl PortalKeys {
|
|
86
|
+
/// `code` is an evdev key code, the portal's own vocabulary.
|
|
87
|
+
pub fn key(&self, code: i16, down: bool) -> Result<()> {
|
|
88
|
+
let state = if down { KeyState::Pressed } else { KeyState::Released };
|
|
89
|
+
self.0
|
|
90
|
+
.send((code as i32, state))
|
|
91
|
+
.map_err(|_| anyhow::anyhow!("the portal keyboard session has ended"))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[cfg(test)]
|
|
96
|
+
mod tests {
|
|
97
|
+
use super::*;
|
|
98
|
+
use std::collections::HashMap;
|
|
99
|
+
use std::process::{Child, Command, Stdio};
|
|
100
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
101
|
+
use std::sync::{Arc, Mutex};
|
|
102
|
+
use zbus::zvariant::{ObjectPath, OwnedValue, Value};
|
|
103
|
+
use zbus::{connection::Builder, interface, Connection};
|
|
104
|
+
|
|
105
|
+
type Log = Arc<Mutex<Vec<String>>>;
|
|
106
|
+
|
|
107
|
+
struct Bus(Child);
|
|
108
|
+
|
|
109
|
+
impl Drop for Bus {
|
|
110
|
+
fn drop(&mut self) {
|
|
111
|
+
let _ = self.0.kill();
|
|
112
|
+
let _ = self.0.wait();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
struct MockSession(Log);
|
|
117
|
+
|
|
118
|
+
#[interface(name = "org.freedesktop.portal.Session")]
|
|
119
|
+
impl MockSession {
|
|
120
|
+
fn close(&self) {
|
|
121
|
+
self.0.lock().unwrap().push("close".into());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// A RemoteDesktop portal that grants what `devices` says and records
|
|
126
|
+
/// every call, the way a compositor's portal would.
|
|
127
|
+
struct MockPortal {
|
|
128
|
+
devices: Arc<AtomicU32>,
|
|
129
|
+
log: Log,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl MockPortal {
|
|
133
|
+
fn token(options: &HashMap<String, OwnedValue>, key: &str) -> String {
|
|
134
|
+
String::try_from(options[key].try_clone().unwrap()).unwrap()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async fn respond(
|
|
138
|
+
connection: &Connection,
|
|
139
|
+
sender: &str,
|
|
140
|
+
options: &HashMap<String, OwnedValue>,
|
|
141
|
+
results: HashMap<&str, Value<'_>>,
|
|
142
|
+
) -> ObjectPath<'static> {
|
|
143
|
+
let path = ObjectPath::try_from(format!(
|
|
144
|
+
"/org/freedesktop/portal/desktop/request/{}/{}",
|
|
145
|
+
sender.trim_start_matches(':').replace('.', "_"),
|
|
146
|
+
Self::token(options, "handle_token")
|
|
147
|
+
))
|
|
148
|
+
.unwrap();
|
|
149
|
+
connection
|
|
150
|
+
.emit_signal(
|
|
151
|
+
None::<&str>,
|
|
152
|
+
&path,
|
|
153
|
+
"org.freedesktop.portal.Request",
|
|
154
|
+
"Response",
|
|
155
|
+
&(0u32, results),
|
|
156
|
+
)
|
|
157
|
+
.await
|
|
158
|
+
.unwrap();
|
|
159
|
+
path
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[interface(name = "org.freedesktop.portal.RemoteDesktop")]
|
|
164
|
+
impl MockPortal {
|
|
165
|
+
#[zbus(property)]
|
|
166
|
+
fn available_device_types(&self) -> u32 {
|
|
167
|
+
self.devices.load(Ordering::SeqCst)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[zbus(property, name = "version")]
|
|
171
|
+
fn version(&self) -> u32 {
|
|
172
|
+
2
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async fn create_session(
|
|
176
|
+
&self,
|
|
177
|
+
#[zbus(connection)] connection: &Connection,
|
|
178
|
+
#[zbus(header)] header: zbus::message::Header<'_>,
|
|
179
|
+
options: HashMap<String, OwnedValue>,
|
|
180
|
+
) -> ObjectPath<'static> {
|
|
181
|
+
let sender = header.sender().unwrap().to_string();
|
|
182
|
+
let session = format!(
|
|
183
|
+
"/org/freedesktop/portal/desktop/session/{}/{}",
|
|
184
|
+
sender.trim_start_matches(':').replace('.', "_"),
|
|
185
|
+
Self::token(&options, "session_handle_token")
|
|
186
|
+
);
|
|
187
|
+
connection
|
|
188
|
+
.object_server()
|
|
189
|
+
.at(session.as_str(), MockSession(self.log.clone()))
|
|
190
|
+
.await
|
|
191
|
+
.unwrap();
|
|
192
|
+
let results = HashMap::from([("session_handle", Value::from(session))]);
|
|
193
|
+
Self::respond(connection, &sender, &options, results).await
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async fn select_devices(
|
|
197
|
+
&self,
|
|
198
|
+
#[zbus(connection)] connection: &Connection,
|
|
199
|
+
#[zbus(header)] header: zbus::message::Header<'_>,
|
|
200
|
+
_session: ObjectPath<'_>,
|
|
201
|
+
options: HashMap<String, OwnedValue>,
|
|
202
|
+
) -> ObjectPath<'static> {
|
|
203
|
+
let sender = header.sender().unwrap().to_string();
|
|
204
|
+
Self::respond(connection, &sender, &options, HashMap::new()).await
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async fn start(
|
|
208
|
+
&self,
|
|
209
|
+
#[zbus(connection)] connection: &Connection,
|
|
210
|
+
#[zbus(header)] header: zbus::message::Header<'_>,
|
|
211
|
+
_session: ObjectPath<'_>,
|
|
212
|
+
_parent: String,
|
|
213
|
+
options: HashMap<String, OwnedValue>,
|
|
214
|
+
) -> ObjectPath<'static> {
|
|
215
|
+
let sender = header.sender().unwrap().to_string();
|
|
216
|
+
let results = HashMap::from([("devices", Value::from(self.devices.load(Ordering::SeqCst)))]);
|
|
217
|
+
Self::respond(connection, &sender, &options, results).await
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn notify_keyboard_keycode(
|
|
221
|
+
&self,
|
|
222
|
+
_session: ObjectPath<'_>,
|
|
223
|
+
_options: HashMap<String, OwnedValue>,
|
|
224
|
+
keycode: i32,
|
|
225
|
+
state: u32,
|
|
226
|
+
) {
|
|
227
|
+
self.log.lock().unwrap().push(format!("key {keycode} {state}"));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async fn serve(devices: Arc<AtomicU32>) -> (Bus, Connection, Log) {
|
|
232
|
+
let mut daemon = Command::new("dbus-daemon")
|
|
233
|
+
.args(["--session", "--nofork", "--print-address=1"])
|
|
234
|
+
.stdout(Stdio::piped())
|
|
235
|
+
.spawn()
|
|
236
|
+
.expect("dbus-daemon");
|
|
237
|
+
let mut address = String::new();
|
|
238
|
+
std::io::BufRead::read_line(
|
|
239
|
+
&mut std::io::BufReader::new(daemon.stdout.take().unwrap()),
|
|
240
|
+
&mut address,
|
|
241
|
+
)
|
|
242
|
+
.unwrap();
|
|
243
|
+
let bus = Bus(daemon);
|
|
244
|
+
let log = Log::default();
|
|
245
|
+
let connection = Builder::address(address.trim())
|
|
246
|
+
.unwrap()
|
|
247
|
+
.name("org.freedesktop.portal.Desktop")
|
|
248
|
+
.unwrap()
|
|
249
|
+
.serve_at(
|
|
250
|
+
"/org/freedesktop/portal/desktop",
|
|
251
|
+
MockPortal { devices, log: log.clone() },
|
|
252
|
+
)
|
|
253
|
+
.unwrap()
|
|
254
|
+
.build()
|
|
255
|
+
.await
|
|
256
|
+
.unwrap();
|
|
257
|
+
// The proxies read the session bus from the environment.
|
|
258
|
+
std::env::set_var("DBUS_SESSION_BUS_ADDRESS", address.trim());
|
|
259
|
+
(bus, connection, log)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// ashpd caches its session-bus connection per process, so the scenario
|
|
263
|
+
/// runs in a child process of its own rather than beside the other portal
|
|
264
|
+
/// test.
|
|
265
|
+
#[test]
|
|
266
|
+
fn keys_reach_the_portal_in_order_and_the_session_closes() {
|
|
267
|
+
if std::env::var_os("DESKLINK_PORTAL_MOCK_CHILD").is_none() {
|
|
268
|
+
let status = Command::new(std::env::current_exe().unwrap())
|
|
269
|
+
.args(["--nocapture", "--exact", "input_portal::tests::keys_reach_the_portal_in_order_and_the_session_closes"])
|
|
270
|
+
.env("DESKLINK_PORTAL_MOCK_CHILD", "1")
|
|
271
|
+
.status()
|
|
272
|
+
.unwrap();
|
|
273
|
+
assert!(status.success());
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
tokio::runtime::Builder::new_multi_thread()
|
|
277
|
+
.enable_all()
|
|
278
|
+
.build()
|
|
279
|
+
.unwrap()
|
|
280
|
+
.block_on(scenario());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// What the capabilities report selects on a host without `/dev/uinput` access.
|
|
284
|
+
async fn mechanism() -> String {
|
|
285
|
+
let no_uinput = crate::input::InputUnavailable {
|
|
286
|
+
reason: "no /dev/uinput".into(),
|
|
287
|
+
remedy: String::new(),
|
|
288
|
+
};
|
|
289
|
+
report(Err(no_uinput))["mechanism"].to_string()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn report(uinput: Result<(), crate::input::InputUnavailable>) -> serde_json::Value {
|
|
293
|
+
let input = crate::session::capabilities_with(uinput)["input"].clone();
|
|
294
|
+
println!("capabilities input: {input}");
|
|
295
|
+
input
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Keys sent through the adapter reach the portal in order, and dropping
|
|
299
|
+
/// the adapter closes the portal session. A portal that offers no keyboard
|
|
300
|
+
/// is not selected, so the caller falls back to uinput.
|
|
301
|
+
async fn scenario() {
|
|
302
|
+
let devices = Arc::new(AtomicU32::new(2));
|
|
303
|
+
let (_bus, _service, log) = serve(devices.clone()).await;
|
|
304
|
+
assert!(!advertised().await);
|
|
305
|
+
assert!(open().await.is_err());
|
|
306
|
+
assert!(log.lock().unwrap().is_empty());
|
|
307
|
+
assert_eq!(mechanism().await, "\"inputtino/uinput\"");
|
|
308
|
+
|
|
309
|
+
devices.store(1, Ordering::SeqCst);
|
|
310
|
+
assert!(advertised().await);
|
|
311
|
+
// Where uinput works too, it stays the path and the pointer keeps working.
|
|
312
|
+
let both = report(Ok(()));
|
|
313
|
+
assert_eq!(both["mechanism"], "inputtino/uinput");
|
|
314
|
+
assert_eq!((&both["pointer"], &both["wheel"], &both["keyboard"]), (&true.into(), &true.into(), &true.into()));
|
|
315
|
+
assert_eq!(mechanism().await, "\"portal-remotedesktop\"");
|
|
316
|
+
let keys = open().await.expect("keyboard session");
|
|
317
|
+
// No host-wide virtual device while the portal carries the keys.
|
|
318
|
+
for fd in std::fs::read_dir("/proc/self/fd").unwrap().flatten() {
|
|
319
|
+
let target = std::fs::read_link(fd.path()).unwrap_or_default();
|
|
320
|
+
assert!(!target.starts_with("/dev/uinput"), "uinput open: {target:?}");
|
|
321
|
+
}
|
|
322
|
+
for (code, down) in [(35, true), (35, false), (30, true), (30, false)] {
|
|
323
|
+
keys.key(code, down).unwrap();
|
|
324
|
+
}
|
|
325
|
+
drop(keys);
|
|
326
|
+
for _ in 0..100 {
|
|
327
|
+
if log.lock().unwrap().last().is_some_and(|last| last == "close") {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
|
331
|
+
}
|
|
332
|
+
assert_eq!(
|
|
333
|
+
*log.lock().unwrap(),
|
|
334
|
+
["key 35 1", "key 35 0", "key 30 1", "key 30 0", "close"]
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
package/engine/src/main.rs
CHANGED
|
@@ -56,6 +56,8 @@ mod hyprland;
|
|
|
56
56
|
mod indicator;
|
|
57
57
|
#[cfg(target_os = "linux")]
|
|
58
58
|
mod input;
|
|
59
|
+
#[cfg(target_os = "linux")]
|
|
60
|
+
mod input_portal;
|
|
59
61
|
#[cfg(target_os = "macos")]
|
|
60
62
|
#[path = "input_mac.rs"]
|
|
61
63
|
mod input;
|
|
@@ -986,91 +988,3 @@ fn main() {
|
|
|
986
988
|
fn main() {
|
|
987
989
|
std::process::exit(win::run());
|
|
988
990
|
}
|
|
989
|
-
|
|
990
|
-
#[cfg(all(test, target_os = "linux"))]
|
|
991
|
-
mod tests {
|
|
992
|
-
use super::*;
|
|
993
|
-
|
|
994
|
-
#[tokio::test]
|
|
995
|
-
async fn a_consumer_speaking_another_protocol_version_is_refused() {
|
|
996
|
-
let mut hello_seen = false;
|
|
997
|
-
let mut current = None;
|
|
998
|
-
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
999
|
-
let request = Request {
|
|
1000
|
-
id: Some(1),
|
|
1001
|
-
method: String::from("hello"),
|
|
1002
|
-
params: serde_json::json!({ "protocol": 99, "client": "test" }),
|
|
1003
|
-
};
|
|
1004
|
-
let error = dispatch(&request, &mut hello_seen, &mut current, &events)
|
|
1005
|
-
.await
|
|
1006
|
-
.unwrap_err();
|
|
1007
|
-
assert_eq!(error.code, "unsupported-protocol");
|
|
1008
|
-
assert!(!hello_seen, "a refused handshake must not open the gate");
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
#[test]
|
|
1012
|
-
fn cursor_notice_preserves_source_coordinates_visibility_and_clock() {
|
|
1013
|
-
let line = render_event(session::Notice {
|
|
1014
|
-
session_id: String::from("cursor-session"),
|
|
1015
|
-
event: session::SessionEvent::Cursor {
|
|
1016
|
-
position: protocol::CursorPosition {
|
|
1017
|
-
x: 32,
|
|
1018
|
-
y: 64,
|
|
1019
|
-
visible: false,
|
|
1020
|
-
hotspot: None,
|
|
1021
|
-
timestamp_us: 123456,
|
|
1022
|
-
},
|
|
1023
|
-
},
|
|
1024
|
-
})
|
|
1025
|
-
.unwrap();
|
|
1026
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
1027
|
-
assert_eq!(
|
|
1028
|
-
parsed,
|
|
1029
|
-
serde_json::json!({"event":"session.cursor","params":{"sessionId":"cursor-session","x":32,"y":64,"visible":false,"timestamp_us":123456}})
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
#[test]
|
|
1034
|
-
fn the_skipped_ice_server_warning_names_the_url_once() {
|
|
1035
|
-
let line = render_event(session::Notice {
|
|
1036
|
-
session_id: String::from("session-1"),
|
|
1037
|
-
event: session::SessionEvent::IceServerSkipped {
|
|
1038
|
-
url: String::from("turn:turn.example.com:3478?transport=tcp"),
|
|
1039
|
-
},
|
|
1040
|
-
})
|
|
1041
|
-
.expect("a skipped-URL warning is renderable");
|
|
1042
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
1043
|
-
assert_eq!(parsed["event"], "session.iceServerSkipped");
|
|
1044
|
-
assert_eq!(
|
|
1045
|
-
parsed["params"],
|
|
1046
|
-
serde_json::json!({
|
|
1047
|
-
"sessionId": "session-1",
|
|
1048
|
-
"url": "turn:turn.example.com:3478?transport=tcp"
|
|
1049
|
-
})
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
#[tokio::test]
|
|
1054
|
-
async fn the_state_notification_carries_exactly_what_the_document_promises() {
|
|
1055
|
-
let line = render_event(session::Notice {
|
|
1056
|
-
session_id: String::from("session-1"),
|
|
1057
|
-
event: session::SessionEvent::State {
|
|
1058
|
-
capture: "streaming",
|
|
1059
|
-
transport: String::from("connected"),
|
|
1060
|
-
first_frame: true,
|
|
1061
|
-
},
|
|
1062
|
-
})
|
|
1063
|
-
.expect("a state event is renderable");
|
|
1064
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
1065
|
-
assert_eq!(parsed["event"], "session.state");
|
|
1066
|
-
assert_eq!(
|
|
1067
|
-
parsed["params"],
|
|
1068
|
-
serde_json::json!({
|
|
1069
|
-
"sessionId": "session-1",
|
|
1070
|
-
"capture": "streaming",
|
|
1071
|
-
"transport": "connected",
|
|
1072
|
-
"firstFrame": true
|
|
1073
|
-
})
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
package/engine/src/observe.rs
CHANGED
|
@@ -59,36 +59,3 @@ pub fn status(result: Option<&Result<Tap, Unavailable>>) -> serde_json::Value {
|
|
|
59
59
|
}),
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
#[cfg(test)]
|
|
64
|
-
mod tests {
|
|
65
|
-
use super::*;
|
|
66
|
-
|
|
67
|
-
#[test]
|
|
68
|
-
#[cfg(not(target_os = "macos"))]
|
|
69
|
-
fn a_platform_that_cannot_observe_says_so_and_delivers_nothing() {
|
|
70
|
-
let delivered = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
71
|
-
let counted = delivered.clone();
|
|
72
|
-
let sink: Sink = Arc::new(move |_| {
|
|
73
|
-
counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
74
|
-
});
|
|
75
|
-
let refused = start(0, (1920, 1080), sink);
|
|
76
|
-
let status = status(Some(&refused));
|
|
77
|
-
assert_eq!(status["observing"], false);
|
|
78
|
-
assert_eq!(status["code"], "unsupported-platform");
|
|
79
|
-
assert_eq!(delivered.load(std::sync::atomic::Ordering::SeqCst), 0);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
#[test]
|
|
83
|
-
fn a_refusal_carries_its_code_reason_and_remedy() {
|
|
84
|
-
let refused: Result<Tap, Unavailable> = Err(Unavailable {
|
|
85
|
-
code: "missing-input-monitoring",
|
|
86
|
-
reason: String::from("reason"),
|
|
87
|
-
remedy: String::from("remedy"),
|
|
88
|
-
});
|
|
89
|
-
assert_eq!(
|
|
90
|
-
status(Some(&refused)),
|
|
91
|
-
serde_json::json!({"observing": false, "code": "missing-input-monitoring", "reason": "reason", "remedy": "remedy"})
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|