@desklink/host 0.5.9 → 0.5.11
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/README.md +19 -44
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +2 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +1 -0
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/PROTOCOL.md +56 -0
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +3 -1
- package/engine/native/agent_overlay_mac.m +19 -12
- package/engine/native/agent_overlay_wayland.c +15 -12
- package/engine/native/agent_overlay_x11.c +66 -17
- package/engine/src/input_mac.rs +433 -0
- package/engine/src/keymap_mac.rs +257 -0
- package/engine/src/mac.rs +8 -0
- package/engine/src/main.rs +12 -1
- package/engine/src/observe.rs +94 -0
- package/engine/src/portal.rs +83 -1
- package/engine/src/protocol.rs +30 -1
- package/engine/src/session.rs +59 -0
- package/package.json +3 -3
package/engine/src/keymap_mac.rs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
|
+
#[cfg(desklink_vpx)]
|
|
3
|
+
use crate::protocol::ObservedInput;
|
|
2
4
|
|
|
3
5
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
4
6
|
pub struct Keystroke {
|
|
@@ -182,3 +184,258 @@ fn us_ansi_key(character: char) -> Option<(i16, bool)> {
|
|
|
182
184
|
_ => return None,
|
|
183
185
|
})
|
|
184
186
|
}
|
|
187
|
+
|
|
188
|
+
/// Event types of a CGEventTap that carry a key.
|
|
189
|
+
#[cfg(desklink_vpx)]
|
|
190
|
+
pub mod key_event {
|
|
191
|
+
pub const DOWN: u32 = 10;
|
|
192
|
+
pub const UP: u32 = 11;
|
|
193
|
+
pub const FLAGS_CHANGED: u32 = 12;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Generic modifier bits of a CGEvent's flags.
|
|
197
|
+
#[cfg(desklink_vpx)]
|
|
198
|
+
const SHIFT: u64 = 0x2_0000;
|
|
199
|
+
#[cfg(desklink_vpx)]
|
|
200
|
+
const CONTROL: u64 = 0x4_0000;
|
|
201
|
+
#[cfg(desklink_vpx)]
|
|
202
|
+
const ALT: u64 = 0x8_0000;
|
|
203
|
+
#[cfg(desklink_vpx)]
|
|
204
|
+
const META: u64 = 0x10_0000;
|
|
205
|
+
|
|
206
|
+
/// A modifier key: its name, and the bit of the event flags that is set while
|
|
207
|
+
/// this key (not just its twin on the other side) is held. Caps Lock and Fn have
|
|
208
|
+
/// no per-side bit, so theirs is the generic one.
|
|
209
|
+
#[cfg(desklink_vpx)]
|
|
210
|
+
const MODIFIER_KEYS: [(i16, &str, u64); 10] = [
|
|
211
|
+
(59, "Control", 0x1), (62, "ControlRight", 0x2000), (56, "Shift", 0x2),
|
|
212
|
+
(60, "ShiftRight", 0x4), (58, "Alt", 0x20), (61, "AltRight", 0x40),
|
|
213
|
+
(55, "Meta", 0x8), (54, "MetaRight", 0x10), (57, "CapsLock", 0x1_0000),
|
|
214
|
+
(63, "Fn", 0x80_0000),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
/// Keys that have a name rather than a character, following `KeyboardEvent.key`.
|
|
218
|
+
/// Every name here that the control channel accepts maps to the same virtual key
|
|
219
|
+
/// code through `named_key`.
|
|
220
|
+
#[cfg(desklink_vpx)]
|
|
221
|
+
const NAMED_KEYS: [(i16, &str); 36] = [
|
|
222
|
+
(36, "Enter"), (76, "Enter"), (48, "Tab"), (53, "Escape"),
|
|
223
|
+
(51, "Backspace"), (117, "Delete"), (126, "ArrowUp"), (125, "ArrowDown"),
|
|
224
|
+
(123, "ArrowLeft"), (124, "ArrowRight"), (115, "Home"), (119, "End"),
|
|
225
|
+
(116, "PageUp"), (121, "PageDown"), (122, "F1"), (120, "F2"),
|
|
226
|
+
(99, "F3"), (118, "F4"), (96, "F5"), (97, "F6"),
|
|
227
|
+
(98, "F7"), (100, "F8"), (101, "F9"), (109, "F10"),
|
|
228
|
+
(103, "F11"), (111, "F12"), (105, "F13"), (107, "F14"),
|
|
229
|
+
(113, "F15"), (106, "F16"), (64, "F17"), (79, "F18"),
|
|
230
|
+
(80, "F19"), (90, "F20"), (114, "Help"), (110, "ContextMenu"),
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
/// Names of the modifiers held in `flags`, in the order the control channel's
|
|
234
|
+
/// own examples use.
|
|
235
|
+
#[cfg(desklink_vpx)]
|
|
236
|
+
pub fn modifier_names(flags: u64) -> Vec<String> {
|
|
237
|
+
[
|
|
238
|
+
(CONTROL, "Control"),
|
|
239
|
+
(ALT, "Alt"),
|
|
240
|
+
(SHIFT, "Shift"),
|
|
241
|
+
(META, "Meta"),
|
|
242
|
+
]
|
|
243
|
+
.into_iter()
|
|
244
|
+
.filter(|(bit, _)| flags & bit != 0)
|
|
245
|
+
.map(|(_, name)| name.to_owned())
|
|
246
|
+
.collect()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// The character a key makes on the US ANSI map this engine injects with. Only
|
|
250
|
+
/// a fallback: a control chord, or an event the system gave no text, has none.
|
|
251
|
+
#[cfg(desklink_vpx)]
|
|
252
|
+
fn base_character(code: i16, shift: bool) -> Option<char> {
|
|
253
|
+
(' '..='~').find(|character| us_ansi_key(*character) == Some((code, shift)))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// The single printable character a key produced on the layout in use.
|
|
257
|
+
#[cfg(desklink_vpx)]
|
|
258
|
+
fn printable(text: &str) -> Option<char> {
|
|
259
|
+
let mut characters = text.chars();
|
|
260
|
+
let character = characters.next()?;
|
|
261
|
+
// U+F700..U+F8FF is where AppKit puts the function and arrow keys.
|
|
262
|
+
(characters.next().is_none() && !character.is_control() && !('\u{f700}'..='\u{f8ff}').contains(&character))
|
|
263
|
+
.then_some(character)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// What a CGEventTap key event means, in the control channel's `key` vocabulary.
|
|
267
|
+
///
|
|
268
|
+
/// `text` is what the event types on the active layout. Auto-repeat is not an
|
|
269
|
+
/// event: a held key is one down and one up. A modifier arrives as a flags
|
|
270
|
+
/// change, whose direction is its own bit being set.
|
|
271
|
+
#[cfg(desklink_vpx)]
|
|
272
|
+
pub fn observed_key(
|
|
273
|
+
event_type: u32,
|
|
274
|
+
code: i16,
|
|
275
|
+
flags: u64,
|
|
276
|
+
autorepeat: bool,
|
|
277
|
+
text: &str,
|
|
278
|
+
) -> Option<ObservedInput> {
|
|
279
|
+
let modifiers = modifier_names(flags);
|
|
280
|
+
if event_type == key_event::FLAGS_CHANGED {
|
|
281
|
+
let (_, name, bit) = MODIFIER_KEYS.iter().find(|(key, _, _)| *key == code)?;
|
|
282
|
+
return Some(ObservedInput::Key {
|
|
283
|
+
name: Some((*name).to_owned()),
|
|
284
|
+
character: None,
|
|
285
|
+
down: flags & bit != 0,
|
|
286
|
+
modifiers,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
let down = match event_type {
|
|
290
|
+
key_event::DOWN if autorepeat => return None,
|
|
291
|
+
key_event::DOWN => true,
|
|
292
|
+
key_event::UP => false,
|
|
293
|
+
_ => return None,
|
|
294
|
+
};
|
|
295
|
+
let named = NAMED_KEYS.iter().find(|(key, _)| *key == code);
|
|
296
|
+
let character = if named.is_some() {
|
|
297
|
+
None
|
|
298
|
+
} else {
|
|
299
|
+
printable(text).or_else(|| base_character(code, flags & SHIFT != 0))
|
|
300
|
+
};
|
|
301
|
+
let name = match (named, character) {
|
|
302
|
+
(Some((_, name)), _) => Some((*name).to_owned()),
|
|
303
|
+
(None, Some(_)) => None,
|
|
304
|
+
(None, None) => Some(String::from("Unidentified")),
|
|
305
|
+
};
|
|
306
|
+
Some(ObservedInput::Key {
|
|
307
|
+
name,
|
|
308
|
+
character: character.map(String::from),
|
|
309
|
+
down,
|
|
310
|
+
modifiers,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[cfg(all(test, desklink_vpx))]
|
|
315
|
+
mod tests {
|
|
316
|
+
use super::*;
|
|
317
|
+
|
|
318
|
+
fn key(
|
|
319
|
+
event_type: u32,
|
|
320
|
+
code: i16,
|
|
321
|
+
flags: u64,
|
|
322
|
+
autorepeat: bool,
|
|
323
|
+
text: &str,
|
|
324
|
+
) -> Option<ObservedInput> {
|
|
325
|
+
observed_key(event_type, code, flags, autorepeat, text)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn a_letter_is_a_character_with_the_modifier_state_after_the_event() {
|
|
330
|
+
assert_eq!(
|
|
331
|
+
key(key_event::DOWN, 0, SHIFT, false, "A"),
|
|
332
|
+
Some(ObservedInput::Key {
|
|
333
|
+
name: None,
|
|
334
|
+
character: Some(String::from("A")),
|
|
335
|
+
down: true,
|
|
336
|
+
modifiers: vec![String::from("Shift")],
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#[test]
|
|
342
|
+
fn a_named_key_carries_no_character() {
|
|
343
|
+
assert_eq!(
|
|
344
|
+
key(key_event::UP, 36, 0, false, "\r"),
|
|
345
|
+
Some(ObservedInput::Key {
|
|
346
|
+
name: Some(String::from("Enter")),
|
|
347
|
+
character: None,
|
|
348
|
+
down: false,
|
|
349
|
+
modifiers: Vec::new(),
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn a_chord_with_no_text_falls_back_to_the_key_it_is() {
|
|
356
|
+
// Control-C types U+0003, which is not a character a keycap can show.
|
|
357
|
+
let Some(ObservedInput::Key {
|
|
358
|
+
character,
|
|
359
|
+
modifiers,
|
|
360
|
+
..
|
|
361
|
+
}) = key(key_event::DOWN, 8, CONTROL, false, "\u{3}")
|
|
362
|
+
else {
|
|
363
|
+
panic!("a chord is a key");
|
|
364
|
+
};
|
|
365
|
+
assert_eq!(character.as_deref(), Some("c"));
|
|
366
|
+
assert_eq!(modifiers, vec![String::from("Control")]);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn an_event_with_no_text_is_still_the_shifted_character() {
|
|
371
|
+
let Some(ObservedInput::Key { character, .. }) =
|
|
372
|
+
key(key_event::DOWN, 4, SHIFT, false, "")
|
|
373
|
+
else {
|
|
374
|
+
panic!("a letter is a key");
|
|
375
|
+
};
|
|
376
|
+
assert_eq!(character.as_deref(), Some("H"));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn a_held_key_is_one_down_and_one_up() {
|
|
381
|
+
assert_eq!(key(key_event::DOWN, 0, 0, true, "a"), None);
|
|
382
|
+
assert!(key(key_event::UP, 0, 0, false, "a").is_some());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn releasing_one_shift_while_the_other_is_held_is_an_up() {
|
|
387
|
+
// Left shift went up; right shift (bit 0x4) and the generic bit remain.
|
|
388
|
+
let Some(ObservedInput::Key {
|
|
389
|
+
name,
|
|
390
|
+
down,
|
|
391
|
+
modifiers,
|
|
392
|
+
..
|
|
393
|
+
}) = key(key_event::FLAGS_CHANGED, 56, SHIFT | 0x4, false, "")
|
|
394
|
+
else {
|
|
395
|
+
panic!("a modifier change is a key");
|
|
396
|
+
};
|
|
397
|
+
assert_eq!(name.as_deref(), Some("Shift"));
|
|
398
|
+
assert!(!down);
|
|
399
|
+
assert_eq!(modifiers, vec![String::from("Shift")]);
|
|
400
|
+
let Some(ObservedInput::Key { down, .. }) =
|
|
401
|
+
key(key_event::FLAGS_CHANGED, 56, SHIFT | 0x2 | 0x4, false, "")
|
|
402
|
+
else {
|
|
403
|
+
panic!("a modifier change is a key");
|
|
404
|
+
};
|
|
405
|
+
assert!(down);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[test]
|
|
409
|
+
fn a_key_with_no_name_and_no_text_is_unidentified_not_empty() {
|
|
410
|
+
let Some(ObservedInput::Key {
|
|
411
|
+
name, character, ..
|
|
412
|
+
}) = key(key_event::DOWN, 127, 0, false, "")
|
|
413
|
+
else {
|
|
414
|
+
panic!("an unknown key is still a key");
|
|
415
|
+
};
|
|
416
|
+
assert_eq!(name.as_deref(), Some("Unidentified"));
|
|
417
|
+
assert_eq!(character, None);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
fn the_private_use_characters_appkit_gives_arrows_are_not_text() {
|
|
422
|
+
assert_eq!(printable("\u{f702}"), None);
|
|
423
|
+
assert_eq!(printable("é"), Some('é'));
|
|
424
|
+
assert_eq!(printable("ab"), None);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#[test]
|
|
428
|
+
fn what_is_observed_is_what_the_control_channel_would_inject() {
|
|
429
|
+
for (code, name) in NAMED_KEYS {
|
|
430
|
+
if let Some(stroke) = named_key(name) {
|
|
431
|
+
// Keypad Enter (76) shares its name with Return (36).
|
|
432
|
+
assert!(stroke.code == code || name == "Enter", "{name}");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (code, name, _) in MODIFIER_KEYS {
|
|
436
|
+
if let Some(injected) = modifier_key(name) {
|
|
437
|
+
assert_eq!(injected, code, "{name}");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
package/engine/src/mac.rs
CHANGED
|
@@ -39,6 +39,8 @@ unsafe extern "C" {
|
|
|
39
39
|
pub(crate) fn CGPreflightScreenCaptureAccess() -> bool;
|
|
40
40
|
pub(crate) fn CGPreflightPostEventAccess() -> bool;
|
|
41
41
|
pub(crate) fn CGRequestPostEventAccess() -> bool;
|
|
42
|
+
pub(crate) fn CGPreflightListenEventAccess() -> bool;
|
|
43
|
+
pub(crate) fn CGRequestListenEventAccess() -> bool;
|
|
42
44
|
fn CGGetActiveDisplayList(max: u32, displays: *mut DisplayId, count: *mut u32) -> i32;
|
|
43
45
|
fn CGMainDisplayID() -> DisplayId;
|
|
44
46
|
fn CGDisplayPixelsWide(display: DisplayId) -> usize;
|
|
@@ -713,6 +715,12 @@ pub fn run() -> i32 {
|
|
|
713
715
|
let _ = crate::input::request_access();
|
|
714
716
|
println!("If the Accessibility alert did not open, allow {} in System Settings › Privacy & Security › Accessibility.", tcc_responsible_app_name());
|
|
715
717
|
}
|
|
718
|
+
if crate::input::observe_probe().is_ok() {
|
|
719
|
+
println!("Input Monitoring permission is already granted to {}.", tcc_responsible_app_name());
|
|
720
|
+
} else {
|
|
721
|
+
let _ = crate::input::request_observe_access();
|
|
722
|
+
println!("If the Input Monitoring alert did not open, allow {} in System Settings › Privacy & Security › Input Monitoring.", tcc_responsible_app_name());
|
|
723
|
+
}
|
|
716
724
|
}
|
|
717
725
|
_ => println!("desklink-host {}\nUSAGE: desklink-host [serve|capabilities|capture-probe|encode-probe|setup-input|version]", env!("CARGO_PKG_VERSION")),
|
|
718
726
|
}
|
package/engine/src/main.rs
CHANGED
|
@@ -26,7 +26,7 @@ mod clipboard;
|
|
|
26
26
|
mod clipboard;
|
|
27
27
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
|
28
28
|
mod convert;
|
|
29
|
-
#[cfg(target_os = "linux")]
|
|
29
|
+
#[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
|
|
30
30
|
mod cursor;
|
|
31
31
|
#[cfg(target_os = "linux")]
|
|
32
32
|
mod encoder;
|
|
@@ -72,6 +72,8 @@ mod keymap;
|
|
|
72
72
|
mod keymap;
|
|
73
73
|
#[cfg(target_os = "macos")]
|
|
74
74
|
mod mac;
|
|
75
|
+
#[cfg(any(target_os = "linux", all(any(target_os = "macos", target_os = "windows"), desklink_vpx)))]
|
|
76
|
+
mod observe;
|
|
75
77
|
#[cfg(any(
|
|
76
78
|
target_os = "linux",
|
|
77
79
|
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
@@ -666,6 +668,13 @@ fn render_event(notice: session::Notice) -> Option<String> {
|
|
|
666
668
|
"session.input",
|
|
667
669
|
serde_json::json!({ "sessionId": session_id, "input": input }),
|
|
668
670
|
),
|
|
671
|
+
session::SessionEvent::Observed {
|
|
672
|
+
timestamp_us,
|
|
673
|
+
input,
|
|
674
|
+
} => (
|
|
675
|
+
"session.input.observed",
|
|
676
|
+
serde_json::json!({ "sessionId": session_id, "timestamp_us": timestamp_us, "input": input }),
|
|
677
|
+
),
|
|
669
678
|
session::SessionEvent::KeyframeRequest { generation } => (
|
|
670
679
|
"session.keyframeRequest",
|
|
671
680
|
serde_json::json!({ "sessionId": session_id, "generation": generation }),
|
|
@@ -752,6 +761,7 @@ async fn dispatch(
|
|
|
752
761
|
agent_indicator: params.agent_indicator,
|
|
753
762
|
local_frames: params.local_frames,
|
|
754
763
|
audio: params.audio,
|
|
764
|
+
observe_input: params.observe_input,
|
|
755
765
|
ice_servers: usable.0,
|
|
756
766
|
};
|
|
757
767
|
let session = session::Session::open(open, events.clone())
|
|
@@ -770,6 +780,7 @@ async fn dispatch(
|
|
|
770
780
|
"cursor": session.cursor_info(),
|
|
771
781
|
"agentIndicator": session.agent_indicator(),
|
|
772
782
|
"audio": session.audio_info(),
|
|
783
|
+
"inputObservation": session.observation_info(),
|
|
773
784
|
});
|
|
774
785
|
// URLs the WebRTC stack cannot dial were left out of the peer's
|
|
775
786
|
// configuration; each skipped one is named once instead of
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//! Passive observation of the desktop's own pointer buttons, cursor and keys.
|
|
2
|
+
//!
|
|
3
|
+
//! The engine only listens; events are the ones `docs/PROTOCOL.md` describes. A
|
|
4
|
+
//! platform that cannot observe says why with a typed reason and delivers nothing.
|
|
5
|
+
|
|
6
|
+
use crate::protocol::{CursorPosition, ObservedInput};
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
10
|
+
pub enum Observation {
|
|
11
|
+
Input {
|
|
12
|
+
timestamp_us: u64,
|
|
13
|
+
input: ObservedInput,
|
|
14
|
+
},
|
|
15
|
+
Cursor(CursorPosition),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub type Sink = Arc<dyn Fn(Observation) + Send + Sync + 'static>;
|
|
19
|
+
|
|
20
|
+
/// Keeps an observer running; dropping it stops it.
|
|
21
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
22
|
+
pub struct Tap {
|
|
23
|
+
pub mechanism: &'static str,
|
|
24
|
+
pub guard: Box<dyn Send>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Why the desktop's input cannot be observed, and what the owner can do.
|
|
28
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
29
|
+
pub struct Unavailable {
|
|
30
|
+
pub code: &'static str,
|
|
31
|
+
pub reason: String,
|
|
32
|
+
pub remedy: String,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[cfg(target_os = "macos")]
|
|
36
|
+
pub fn start(display_id: u32, pixels: (usize, usize), sink: Sink) -> Result<Tap, Unavailable> {
|
|
37
|
+
crate::input::observe(display_id, pixels, sink)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[cfg(not(target_os = "macos"))]
|
|
41
|
+
pub fn start(_: u32, _: (usize, usize), _: Sink) -> Result<Tap, Unavailable> {
|
|
42
|
+
Err(Unavailable {
|
|
43
|
+
code: "unsupported-platform",
|
|
44
|
+
reason: String::from("observing the desktop's own input is only implemented on macOS"),
|
|
45
|
+
remedy: String::from("none: this platform delivers no input events"),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// The `inputObservation` member of the `session.open` reply.
|
|
50
|
+
pub fn status(result: Option<&Result<Tap, Unavailable>>) -> serde_json::Value {
|
|
51
|
+
match result {
|
|
52
|
+
None => serde_json::json!({"observing": false}),
|
|
53
|
+
Some(Ok(tap)) => serde_json::json!({"observing": true, "mechanism": tap.mechanism}),
|
|
54
|
+
Some(Err(error)) => serde_json::json!({
|
|
55
|
+
"observing": false,
|
|
56
|
+
"code": error.code,
|
|
57
|
+
"reason": error.reason,
|
|
58
|
+
"remedy": error.remedy,
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
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
|
+
}
|
package/engine/src/portal.rs
CHANGED
|
@@ -47,13 +47,20 @@ pub async fn open_display(_: Option<u32>) -> Result<PortalSession> {
|
|
|
47
47
|
anyhow::bail!("display selection is only supported on macOS")
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/// The portal service answered without a ScreenCast interface. That happens
|
|
51
|
+
/// when xdg-desktop-portal started before the compositor's portal and cached an
|
|
52
|
+
/// interface list without it; only a restart makes it look again.
|
|
53
|
+
const NO_SCREENCAST: &str = "compositor has no ScreenCast portal: xdg-desktop-portal does not export \
|
|
54
|
+
org.freedesktop.portal.ScreenCast. If it started before the compositor's portal, run \
|
|
55
|
+
`systemctl --user restart xdg-desktop-portal` once the compositor portal is running";
|
|
56
|
+
|
|
50
57
|
pub async fn open(
|
|
51
58
|
restore_token: Option<&str>,
|
|
52
59
|
cursor: crate::protocol::CursorMode,
|
|
53
60
|
) -> Result<PortalSession> {
|
|
54
61
|
let proxy = Screencast::new()
|
|
55
62
|
.await
|
|
56
|
-
.context(
|
|
63
|
+
.context(NO_SCREENCAST)?;
|
|
57
64
|
|
|
58
65
|
if proxy
|
|
59
66
|
.available_source_types()
|
|
@@ -142,3 +149,78 @@ fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
|
|
|
142
149
|
.find(|mode| available.contains(*mode))
|
|
143
150
|
.context("cursor-unavailable: portal cannot leave the cursor out of capture")
|
|
144
151
|
}
|
|
152
|
+
|
|
153
|
+
#[cfg(all(test, target_os = "linux"))]
|
|
154
|
+
mod tests {
|
|
155
|
+
use super::*;
|
|
156
|
+
use std::process::{Child, Command, Stdio};
|
|
157
|
+
use zbus::{connection::Builder, message::Type, MessageStream};
|
|
158
|
+
|
|
159
|
+
struct Bus(Child);
|
|
160
|
+
|
|
161
|
+
impl Drop for Bus {
|
|
162
|
+
fn drop(&mut self) {
|
|
163
|
+
let _ = self.0.kill();
|
|
164
|
+
let _ = self.0.wait();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// A private session bus whose `org.freedesktop.portal.Desktop` owner
|
|
169
|
+
/// answers every property read the way a stale xdg-desktop-portal answers
|
|
170
|
+
/// for ScreenCast: no such interface.
|
|
171
|
+
#[tokio::test]
|
|
172
|
+
async fn open_names_the_fix_when_the_portal_exports_no_screencast() {
|
|
173
|
+
use zbus::export::futures_core::Stream;
|
|
174
|
+
|
|
175
|
+
let mut daemon = Command::new("dbus-daemon")
|
|
176
|
+
.args(["--session", "--nofork", "--print-address=1"])
|
|
177
|
+
.stdout(Stdio::piped())
|
|
178
|
+
.spawn()
|
|
179
|
+
.expect("dbus-daemon");
|
|
180
|
+
let mut address = String::new();
|
|
181
|
+
std::io::BufRead::read_line(
|
|
182
|
+
&mut std::io::BufReader::new(daemon.stdout.take().unwrap()),
|
|
183
|
+
&mut address,
|
|
184
|
+
)
|
|
185
|
+
.unwrap();
|
|
186
|
+
let _bus = Bus(daemon);
|
|
187
|
+
let address = address.trim().to_owned();
|
|
188
|
+
|
|
189
|
+
let fake = Builder::address(address.as_str())
|
|
190
|
+
.unwrap()
|
|
191
|
+
.name("org.freedesktop.portal.Desktop")
|
|
192
|
+
.unwrap()
|
|
193
|
+
.build()
|
|
194
|
+
.await
|
|
195
|
+
.unwrap();
|
|
196
|
+
let service = fake.clone();
|
|
197
|
+
let answering = tokio::spawn(async move {
|
|
198
|
+
let mut messages = MessageStream::from(&service);
|
|
199
|
+
while let Some(Ok(message)) =
|
|
200
|
+
std::future::poll_fn(|cx| std::pin::Pin::new(&mut messages).poll_next(cx)).await
|
|
201
|
+
{
|
|
202
|
+
if message.message_type() != Type::MethodCall {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let _ = service
|
|
206
|
+
.reply_dbus_error(
|
|
207
|
+
&message.header(),
|
|
208
|
+
zbus::fdo::Error::InvalidArgs(
|
|
209
|
+
"No such interface \u{201c}org.freedesktop.portal.ScreenCast\u{201d}".into(),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
.await;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Screencast::new reads the session bus from the environment.
|
|
216
|
+
std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address);
|
|
217
|
+
|
|
218
|
+
let error = match open(None, crate::protocol::CursorMode::Embedded).await {
|
|
219
|
+
Ok(_) => panic!("a portal without ScreenCast must not open a session"),
|
|
220
|
+
Err(error) => format!("{error:#}"),
|
|
221
|
+
};
|
|
222
|
+
answering.abort();
|
|
223
|
+
assert!(error.starts_with("compositor has no ScreenCast portal"), "{error}");
|
|
224
|
+
assert!(error.contains("systemctl --user restart xdg-desktop-portal"), "{error}");
|
|
225
|
+
}
|
|
226
|
+
}
|
package/engine/src/protocol.rs
CHANGED
|
@@ -149,6 +149,11 @@ pub struct OpenParams {
|
|
|
149
149
|
/// cannot be answered is refused.
|
|
150
150
|
#[serde(default)]
|
|
151
151
|
pub audio: bool,
|
|
152
|
+
/// Observe this computer's own pointer buttons, cursor and keys and raise
|
|
153
|
+
/// them as `session.input.observed` and `session.cursor`. Off unless asked:
|
|
154
|
+
/// it reports every key typed on the desktop, in every application.
|
|
155
|
+
#[serde(default)]
|
|
156
|
+
pub observe_input: bool,
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
fn default_local_frames() -> bool {
|
|
@@ -379,7 +384,7 @@ impl ControlMessage {
|
|
|
379
384
|
}
|
|
380
385
|
}
|
|
381
386
|
|
|
382
|
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
387
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
383
388
|
#[serde(rename_all = "snake_case")]
|
|
384
389
|
pub enum PointerPhase {
|
|
385
390
|
Move,
|
|
@@ -388,6 +393,30 @@ pub enum PointerPhase {
|
|
|
388
393
|
Cancel,
|
|
389
394
|
}
|
|
390
395
|
|
|
396
|
+
/// A pointer button or key the person pressed on the observed desktop. The
|
|
397
|
+
/// vocabulary is the control channel's own (`pointer` and `key`), so a consumer
|
|
398
|
+
/// reads what it sent and what it saw the same way. `x`/`y` are in the source's
|
|
399
|
+
/// own pixels (`geometry.source`); `modifiers` is the modifier state after the
|
|
400
|
+
/// event.
|
|
401
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
402
|
+
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
403
|
+
pub enum ObservedInput {
|
|
404
|
+
Pointer {
|
|
405
|
+
phase: PointerPhase,
|
|
406
|
+
x: i64,
|
|
407
|
+
y: i64,
|
|
408
|
+
button: i64,
|
|
409
|
+
},
|
|
410
|
+
Key {
|
|
411
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
412
|
+
name: Option<String>,
|
|
413
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
414
|
+
character: Option<String>,
|
|
415
|
+
down: bool,
|
|
416
|
+
modifiers: Vec<String>,
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
|
|
391
420
|
/// Messages the engine sends back on the control channel.
|
|
392
421
|
#[derive(Debug, Serialize)]
|
|
393
422
|
#[serde(tag = "kind", rename_all = "snake_case")]
|