@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,355 @@
1
+ //! Virtual pointer and keyboard through inputtino (MIT) over `uinput`.
2
+ //!
3
+ //! The engine creates *its own* virtual devices and destroys them when the
4
+ //! session ends. It never reads real input devices, never needs the broad
5
+ //! `input` group for anything but the `uinput` node itself, and never asks for
6
+ //! privileges: if `/dev/uinput` is not writable the backend reports itself
7
+ //! unavailable and the engine still captures.
8
+
9
+ use anyhow::Result;
10
+ use std::ffi::CString;
11
+ use std::os::raw::{c_char, c_int, c_short, c_void};
12
+
13
+ #[repr(C)]
14
+ struct DeviceDefinition {
15
+ name: *const c_char,
16
+ vendor_id: u16,
17
+ product_id: u16,
18
+ version: u16,
19
+ device_phys: *const c_char,
20
+ device_uniq: *const c_char,
21
+ }
22
+
23
+ #[repr(C)]
24
+ struct ErrorHandler {
25
+ handler: Option<extern "C" fn(*const c_char, *mut c_void)>,
26
+ user_data: *mut c_void,
27
+ }
28
+
29
+ enum Mouse {}
30
+ enum Keyboard {}
31
+
32
+ extern "C" {
33
+ fn inputtino_mouse_create(
34
+ device: *const DeviceDefinition,
35
+ handler: *const ErrorHandler,
36
+ ) -> *mut Mouse;
37
+ fn inputtino_mouse_move_absolute(
38
+ mouse: *mut Mouse,
39
+ x: c_int,
40
+ y: c_int,
41
+ screen_width: c_int,
42
+ screen_height: c_int,
43
+ );
44
+ fn inputtino_mouse_press_button(mouse: *mut Mouse, button: c_int);
45
+ fn inputtino_mouse_release_button(mouse: *mut Mouse, button: c_int);
46
+ fn inputtino_mouse_scroll_vertical(mouse: *mut Mouse, distance: c_int);
47
+ fn inputtino_mouse_scroll_horizontal(mouse: *mut Mouse, distance: c_int);
48
+ fn inputtino_mouse_destroy(mouse: *mut Mouse);
49
+
50
+ fn inputtino_keyboard_create(
51
+ device: *const DeviceDefinition,
52
+ handler: *const ErrorHandler,
53
+ ) -> *mut Keyboard;
54
+ fn inputtino_keyboard_press(keyboard: *mut Keyboard, key_code: c_short);
55
+ fn inputtino_keyboard_release(keyboard: *mut Keyboard, key_code: c_short);
56
+ fn inputtino_keyboard_destroy(keyboard: *mut Keyboard);
57
+ fn dl_inputtino_keycode(evdev_code: c_short) -> c_short;
58
+ }
59
+
60
+ /// Convert the engine's physical key identity at the native injector boundary.
61
+ pub fn native_keycode(code: i16) -> Result<i16> {
62
+ let native = unsafe { dl_inputtino_keycode(code) };
63
+ anyhow::ensure!(
64
+ native >= 0,
65
+ "the input backend cannot emit physical key {code}"
66
+ );
67
+ Ok(native)
68
+ }
69
+
70
+ /// Pointer buttons, mirroring inputtino's own enum order.
71
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
72
+ pub enum Button {
73
+ Left = 0,
74
+ Middle = 1,
75
+ Right = 2,
76
+ }
77
+
78
+ impl Button {
79
+ pub fn from_number(number: i64) -> Button {
80
+ match number {
81
+ 2 => Button::Middle,
82
+ 3 => Button::Right,
83
+ _ => Button::Left,
84
+ }
85
+ }
86
+ }
87
+
88
+ /// Linux evdev key codes, named only where the engine uses them directly.
89
+ pub mod keycode {
90
+ pub const ESC: i16 = 1;
91
+ pub const BACKSPACE: i16 = 14;
92
+ pub const TAB: i16 = 15;
93
+ pub const ENTER: i16 = 28;
94
+ pub const LEFT_CTRL: i16 = 29;
95
+ pub const LEFT_SHIFT: i16 = 42;
96
+ pub const RIGHT_SHIFT: i16 = 54;
97
+ pub const LEFT_ALT: i16 = 56;
98
+ pub const LEFT_META: i16 = 125;
99
+ pub const RIGHT_CTRL: i16 = 97;
100
+ pub const RIGHT_ALT: i16 = 100;
101
+ pub const RIGHT_META: i16 = 126;
102
+ pub const UP: i16 = 103;
103
+ pub const DOWN: i16 = 108;
104
+ pub const LEFT: i16 = 105;
105
+ pub const RIGHT: i16 = 106;
106
+ pub const HOME: i16 = 102;
107
+ pub const END: i16 = 107;
108
+ pub const PAGE_UP: i16 = 104;
109
+ pub const PAGE_DOWN: i16 = 109;
110
+ pub const DELETE: i16 = 111;
111
+ }
112
+
113
+ /// Why the input backend is unavailable, so the consumer can say something true
114
+ /// instead of showing a dead surface.
115
+ #[derive(Debug, Clone, serde::Serialize)]
116
+ pub struct InputUnavailable {
117
+ pub reason: String,
118
+ pub remedy: String,
119
+ }
120
+
121
+ pub fn probe() -> Result<(), InputUnavailable> {
122
+ match std::fs::OpenOptions::new().write(true).open("/dev/uinput") {
123
+ Ok(_) => Ok(()),
124
+ Err(error) => Err(InputUnavailable {
125
+ reason: format!("/dev/uinput is not writable: {error}"),
126
+ remedy: String::from(
127
+ "run `desklink-host setup-input` to see the one-time, narrowly scoped access rule",
128
+ ),
129
+ }),
130
+ }
131
+ }
132
+
133
+ /// What one session is currently holding down.
134
+ ///
135
+ /// Kept separately from the device that applies it so the release rule — release
136
+ /// exactly what this session pressed, in a defined order, once — is one piece of
137
+ /// testable logic rather than a habit of each backend. A key released that this
138
+ /// session never pressed is the failure mode this prevents: on a shared virtual
139
+ /// keyboard it would lift a modifier someone else is holding.
140
+ #[derive(Debug, Default, Clone, PartialEq, Eq)]
141
+ pub struct HeldState {
142
+ keys: Vec<i16>,
143
+ buttons: Vec<Button>,
144
+ }
145
+
146
+ impl HeldState {
147
+ pub fn key(&mut self, code: i16, down: bool) {
148
+ if down {
149
+ if !self.keys.contains(&code) {
150
+ self.keys.push(code);
151
+ }
152
+ return;
153
+ }
154
+ self.keys.retain(|held| *held != code);
155
+ }
156
+
157
+ pub fn button(&mut self, button: Button, down: bool) {
158
+ if down {
159
+ if !self.buttons.contains(&button) {
160
+ self.buttons.push(button);
161
+ }
162
+ return;
163
+ }
164
+ self.buttons.retain(|held| *held != button);
165
+ }
166
+
167
+ /// Everything to release, and nothing this session did not press. Buttons
168
+ /// first: on a desktop, a pointer button released after a modifier is the
169
+ /// order the user's own hand would produce.
170
+ pub fn release_plan(&mut self) -> (Vec<Button>, Vec<i16>) {
171
+ (
172
+ std::mem::take(&mut self.buttons),
173
+ std::mem::take(&mut self.keys),
174
+ )
175
+ }
176
+ }
177
+
178
+ /// A live pair of virtual devices. Dropping this releases anything still held
179
+ /// and removes the devices, which is what makes a dropped connection safe.
180
+ pub struct InputDevices {
181
+ mouse: *mut Mouse,
182
+ keyboard: *mut Keyboard,
183
+ screen_width: i32,
184
+ screen_height: i32,
185
+ }
186
+
187
+ // Owned by one session and only touched from its input path.
188
+ unsafe impl Send for InputDevices {}
189
+
190
+ impl InputDevices {
191
+ pub fn create(screen_width: i32, screen_height: i32) -> Result<Self> {
192
+ probe().map_err(|unavailable| anyhow::anyhow!(unavailable.reason))?;
193
+ let handler = ErrorHandler {
194
+ handler: None,
195
+ user_data: std::ptr::null_mut(),
196
+ };
197
+ let mouse_name = CString::new("desklink pointer").unwrap();
198
+ let keyboard_name = CString::new("desklink keyboard").unwrap();
199
+ let mouse_def = DeviceDefinition {
200
+ name: mouse_name.as_ptr(),
201
+ vendor_id: 0x1234,
202
+ product_id: 0x0001,
203
+ version: 1,
204
+ device_phys: std::ptr::null(),
205
+ device_uniq: std::ptr::null(),
206
+ };
207
+ let keyboard_def = DeviceDefinition {
208
+ name: keyboard_name.as_ptr(),
209
+ vendor_id: 0x1234,
210
+ product_id: 0x0002,
211
+ version: 1,
212
+ device_phys: std::ptr::null(),
213
+ device_uniq: std::ptr::null(),
214
+ };
215
+
216
+ let mouse = unsafe { inputtino_mouse_create(&mouse_def, &handler) };
217
+ if mouse.is_null() {
218
+ anyhow::bail!("inputtino could not create a virtual pointer");
219
+ }
220
+ let keyboard = unsafe { inputtino_keyboard_create(&keyboard_def, &handler) };
221
+ if keyboard.is_null() {
222
+ unsafe { inputtino_mouse_destroy(mouse) };
223
+ anyhow::bail!("inputtino could not create a virtual keyboard");
224
+ }
225
+
226
+ Ok(Self {
227
+ mouse,
228
+ keyboard,
229
+ screen_width,
230
+ screen_height,
231
+ })
232
+ }
233
+
234
+ pub fn move_absolute(&mut self, x: i64, y: i64) {
235
+ let x = x.clamp(0, self.screen_width as i64 - 1) as c_int;
236
+ let y = y.clamp(0, self.screen_height as i64 - 1) as c_int;
237
+ unsafe {
238
+ inputtino_mouse_move_absolute(self.mouse, x, y, self.screen_width, self.screen_height)
239
+ };
240
+ }
241
+
242
+ pub fn button(&mut self, button: Button, down: bool) {
243
+ if down {
244
+ unsafe { inputtino_mouse_press_button(self.mouse, button as c_int) };
245
+ } else {
246
+ unsafe { inputtino_mouse_release_button(self.mouse, button as c_int) };
247
+ }
248
+ }
249
+
250
+ pub fn scroll(&mut self, dx: f64, dy: f64) {
251
+ // Protocol deltas are down/right, as in DOM wheel events and X11.
252
+ // evdev's vertical wheel has the opposite sign (positive is up).
253
+ // Its high-resolution unit is 120 per detent, not one screen pixel, so a
254
+ // fraction of a detent is a smooth scroll where the client supports it.
255
+ let vertical = (-dy * 120.0).round().clamp(-1200.0, 1200.0) as c_int;
256
+ let horizontal = (dx * 120.0).round().clamp(-1200.0, 1200.0) as c_int;
257
+ if vertical != 0 {
258
+ unsafe { inputtino_mouse_scroll_vertical(self.mouse, vertical) };
259
+ }
260
+ if horizontal != 0 {
261
+ unsafe { inputtino_mouse_scroll_horizontal(self.mouse, horizontal) };
262
+ }
263
+ }
264
+
265
+ pub fn key(&mut self, code: i16, down: bool) -> Result<()> {
266
+ let native_code = native_keycode(code)?;
267
+ if down {
268
+ unsafe { inputtino_keyboard_press(self.keyboard, native_code) };
269
+ } else {
270
+ unsafe { inputtino_keyboard_release(self.keyboard, native_code) };
271
+ }
272
+ Ok(())
273
+ }
274
+ }
275
+
276
+ impl Drop for InputDevices {
277
+ fn drop(&mut self) {
278
+ // Held state is released by the session before it drops the devices; a
279
+ // device removed without that would still leave the kernel-side release
280
+ // to chance.
281
+ if !self.keyboard.is_null() {
282
+ unsafe { inputtino_keyboard_destroy(self.keyboard) };
283
+ self.keyboard = std::ptr::null_mut();
284
+ }
285
+ if !self.mouse.is_null() {
286
+ unsafe { inputtino_mouse_destroy(self.mouse) };
287
+ self.mouse = std::ptr::null_mut();
288
+ }
289
+ }
290
+ }
291
+
292
+ #[cfg(test)]
293
+ mod tests {
294
+ use super::*;
295
+
296
+ #[test]
297
+ fn button_numbers_map_to_the_expected_pointer_buttons() {
298
+ assert_eq!(Button::from_number(1), Button::Left);
299
+ assert_eq!(Button::from_number(2), Button::Middle);
300
+ assert_eq!(Button::from_number(3), Button::Right);
301
+ assert_eq!(Button::from_number(9), Button::Left);
302
+ }
303
+
304
+ #[test]
305
+ fn held_state_releases_exactly_what_this_session_pressed() {
306
+ let mut held = HeldState::default();
307
+ held.key(29, true); // left ctrl
308
+ held.key(46, true); // c
309
+ held.button(Button::Left, true);
310
+ // A repeat press is the same press, not a second one, so the plan still
311
+ // releases it exactly once.
312
+ held.key(29, true);
313
+
314
+ let (buttons, keys) = held.release_plan();
315
+ assert_eq!(buttons, vec![Button::Left]);
316
+ assert_eq!(keys, vec![29, 46]);
317
+ assert!(
318
+ held.release_plan().1.is_empty(),
319
+ "releasing twice releases nothing"
320
+ );
321
+ }
322
+
323
+ #[test]
324
+ fn an_explicit_release_leaves_only_what_is_still_pressed() {
325
+ let mut held = HeldState::default();
326
+ held.key(29, true);
327
+ held.key(46, true);
328
+ held.key(46, false);
329
+ let (buttons, keys) = held.release_plan();
330
+ assert!(buttons.is_empty());
331
+ assert_eq!(keys, vec![29]);
332
+ }
333
+
334
+ /// Proving the backend is really available here is the point: if this host
335
+ /// cannot create the devices, the engine must say so rather than offer a
336
+ /// control surface that does nothing.
337
+ #[test]
338
+ fn this_host_can_create_and_destroy_the_virtual_devices() {
339
+ // This touches the real desktop when uinput is writable. Ordinary
340
+ // suites must never create devices on the owner's active session.
341
+ if std::env::var("DESKLINK_TEST_UINPUT").as_deref() != Ok("1") {
342
+ return;
343
+ }
344
+ match InputDevices::create(2560, 1440) {
345
+ Ok(_devices) => {}
346
+ Err(error) => {
347
+ let unavailable = probe().unwrap_err();
348
+ assert!(
349
+ error.to_string().contains(&unavailable.reason),
350
+ "a creation failure must be the reported availability problem"
351
+ );
352
+ }
353
+ }
354
+ }
355
+ }
@@ -0,0 +1,338 @@
1
+ //! Turning a character or a named key into a Linux key code plus the modifier
2
+ //! presses that reach it on the layout this process compiles.
3
+ //!
4
+ //! The layout comes from the session's `XKB_DEFAULT_*` variables when it exports
5
+ //! them, and from xkbcommon's defaults (`us` on `evdev`/`pc105`) otherwise. It is
6
+ //! not yet the compositor's live layout; `capabilities()` reports the identity
7
+ //! actually compiled so the two can be told apart. A character the compiled
8
+ //! layout cannot produce is refused rather than approximated, which is why the
9
+ //! protocol reports the text capability it actually has.
10
+
11
+ use crate::input::keycode;
12
+ use anyhow::Result;
13
+ use xkbcommon::xkb;
14
+
15
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
16
+ pub struct Keystroke {
17
+ pub code: i16,
18
+ pub shift: bool,
19
+ pub alt_gr: bool,
20
+ pub ctrl: bool,
21
+ pub alt: bool,
22
+ pub meta: bool,
23
+ }
24
+
25
+ impl Keystroke {
26
+ fn simple(code: i16) -> Self {
27
+ Self {
28
+ code,
29
+ shift: false,
30
+ alt_gr: false,
31
+ ctrl: false,
32
+ alt: false,
33
+ meta: false,
34
+ }
35
+ }
36
+
37
+ /// Modifier key codes to press before this keystroke, outermost first.
38
+ pub fn modifiers(&self) -> Vec<i16> {
39
+ let mut held = Vec::new();
40
+ if self.ctrl {
41
+ held.push(keycode::LEFT_CTRL);
42
+ }
43
+ if self.alt {
44
+ held.push(keycode::LEFT_ALT);
45
+ }
46
+ if self.meta {
47
+ held.push(keycode::LEFT_META);
48
+ }
49
+ if self.shift {
50
+ held.push(keycode::LEFT_SHIFT);
51
+ }
52
+ if self.alt_gr {
53
+ held.push(keycode::RIGHT_ALT);
54
+ }
55
+ held
56
+ }
57
+ }
58
+
59
+ /// Keys that have no character of their own.
60
+ pub fn named_key(name: &str) -> Option<Keystroke> {
61
+ let code = match name {
62
+ "Enter" | "Return" => keycode::ENTER,
63
+ "Tab" => keycode::TAB,
64
+ "Escape" => keycode::ESC,
65
+ "Backspace" | "DeleteBackward" => keycode::BACKSPACE,
66
+ "Delete" | "DeleteForward" => keycode::DELETE,
67
+ "ArrowUp" => keycode::UP,
68
+ "ArrowDown" => keycode::DOWN,
69
+ "ArrowLeft" => keycode::LEFT,
70
+ "ArrowRight" => keycode::RIGHT,
71
+ "Home" => keycode::HOME,
72
+ "End" => keycode::END,
73
+ "PageUp" => keycode::PAGE_UP,
74
+ "PageDown" => keycode::PAGE_DOWN,
75
+ _ => return None,
76
+ };
77
+ Some(Keystroke::simple(code))
78
+ }
79
+
80
+ /// A modifier the client asked for by name, resolved against the real key codes.
81
+ pub fn modifier_key(name: &str) -> Option<i16> {
82
+ match name {
83
+ "Control" | "ControlLeft" | "Ctrl" => Some(keycode::LEFT_CTRL),
84
+ "ControlRight" => Some(keycode::RIGHT_CTRL),
85
+ "Shift" | "ShiftLeft" => Some(keycode::LEFT_SHIFT),
86
+ "ShiftRight" => Some(keycode::RIGHT_SHIFT),
87
+ "Alt" | "AltLeft" => Some(keycode::LEFT_ALT),
88
+ "AltRight" | "AltGraph" => Some(keycode::RIGHT_ALT),
89
+ "Meta" | "MetaLeft" | "Super" | "Command" => Some(keycode::LEFT_META),
90
+ "MetaRight" => Some(keycode::RIGHT_META),
91
+ _ => None,
92
+ }
93
+ }
94
+
95
+ // xkbcommon keymaps are immutable once compiled and documented as safe for
96
+ // concurrent reads; the engine only ever reads this one, and only under a mutex.
97
+ unsafe impl Send for Layout {}
98
+ unsafe impl Sync for Layout {}
99
+
100
+ /// The RMLVO names the engine compiles, from the session's `XKB_DEFAULT_*`
101
+ /// variables or xkbcommon's defaults when it exports none.
102
+ pub struct LayoutNames {
103
+ pub rules: String,
104
+ pub model: String,
105
+ pub layout: String,
106
+ pub variant: String,
107
+ pub options: Option<String>,
108
+ }
109
+
110
+ impl LayoutNames {
111
+ pub fn from_environment() -> Self {
112
+ Self::from_lookup(|key| std::env::var(key).ok())
113
+ }
114
+
115
+ /// The identity the capability surface reports, e.g. `us` or `de(nodeadkeys)`.
116
+ pub fn identity(&self) -> String {
117
+ if self.variant.is_empty() {
118
+ self.layout.clone()
119
+ } else {
120
+ format!("{}({})", self.layout, self.variant)
121
+ }
122
+ }
123
+
124
+ fn from_lookup(lookup: impl Fn(&str) -> Option<String>) -> Self {
125
+ let named = |key: &str, fallback: &str| {
126
+ lookup(key)
127
+ .filter(|value| !value.is_empty())
128
+ .unwrap_or_else(|| String::from(fallback))
129
+ };
130
+ Self {
131
+ rules: named("XKB_DEFAULT_RULES", "evdev"),
132
+ model: named("XKB_DEFAULT_MODEL", "pc105"),
133
+ layout: named("XKB_DEFAULT_LAYOUT", "us"),
134
+ variant: named("XKB_DEFAULT_VARIANT", ""),
135
+ options: lookup("XKB_DEFAULT_OPTIONS").filter(|value| !value.is_empty()),
136
+ }
137
+ }
138
+ }
139
+
140
+ /// The active keyboard layout, compiled once per session.
141
+ pub struct Layout {
142
+ keymap: xkb::Keymap,
143
+ shift_mask: xkb::ModMask,
144
+ alt_gr_mask: xkb::ModMask,
145
+ }
146
+
147
+ impl Layout {
148
+ /// Build from the session's RMLVO, falling back to the standard
149
+ /// `evdev`/`pc105`/`us` keymap when it exports none.
150
+ pub fn from_environment() -> Result<Self> {
151
+ Self::from_names(&LayoutNames::from_environment())
152
+ }
153
+
154
+ fn from_names(names: &LayoutNames) -> Result<Self> {
155
+ let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
156
+ let keymap = xkb::Keymap::new_from_names(
157
+ &context,
158
+ &names.rules,
159
+ &names.model,
160
+ &names.layout,
161
+ &names.variant,
162
+ names.options.clone(),
163
+ xkb::KEYMAP_COMPILE_NO_FLAGS,
164
+ )
165
+ .ok_or_else(|| {
166
+ anyhow::anyhow!(
167
+ "xkbcommon could not compile the {} keymap",
168
+ names.identity()
169
+ )
170
+ })?;
171
+ // Level bit 0 is the shift level and bit 1 the AltGr/level-3 shift on
172
+ // every standard XKB layout, and the real modifier index for each name
173
+ // comes from the compiled keymap rather than from an assumption that
174
+ // shift is mod 1.
175
+ let shift_mask = 1u32 << keymap.mod_get_index(xkb::MOD_NAME_SHIFT);
176
+ let alt_gr_mask = 1u32 << keymap.mod_get_index(xkb::MOD_NAME_ALT);
177
+ Ok(Self {
178
+ keymap,
179
+ shift_mask,
180
+ alt_gr_mask,
181
+ })
182
+ }
183
+
184
+ /// Find a key code and modifier combination that produces `character`, by
185
+ /// walking exactly the levels the active layout defines.
186
+ pub fn keystroke_for_char(&self, character: char) -> Option<Keystroke> {
187
+ let wanted = xkb::Keysym::from_char(character);
188
+ let shift = self.shift_mask;
189
+ let alt_gr = self.alt_gr_mask;
190
+ for raw in 9u32..=255u32 {
191
+ let code = xkb::Keycode::new(raw);
192
+ // Only levels 0..=3 map to modifiers this engine can press; a symbol
193
+ // that first appears deeper stays unreachable instead of being typed
194
+ // as the key's base symbol.
195
+ let levels = self.keymap.num_levels_for_key(code, 0).min(4);
196
+ for level in 0..levels {
197
+ let mut modifiers = xkb::ModMask::from(0u32);
198
+ if level & 1 == 1 {
199
+ modifiers |= shift;
200
+ }
201
+ if level & 2 == 2 {
202
+ modifiers |= alt_gr;
203
+ }
204
+ let syms = self.keymap.key_get_syms_by_level(code, 0, level);
205
+ if !syms.contains(&wanted) {
206
+ continue;
207
+ }
208
+ return Some(Keystroke {
209
+ // XKB key codes are evdev codes offset by 8.
210
+ code: (raw - 8) as i16,
211
+ shift: modifiers & shift != 0,
212
+ alt_gr: modifiers & alt_gr != 0,
213
+ ctrl: false,
214
+ alt: false,
215
+ meta: false,
216
+ });
217
+ }
218
+ }
219
+ None
220
+ }
221
+
222
+ /// Apply text as real key events. Returns the characters that this layout
223
+ /// cannot produce, so the caller can report an honest partial result rather
224
+ /// than silently dropping them.
225
+ pub fn plan_text(&self, text: &str) -> (Vec<Vec<Keystroke>>, String) {
226
+ let mut plan = Vec::new();
227
+ let mut unreachable = String::new();
228
+ for character in text.chars() {
229
+ if character == '\n' {
230
+ plan.push(vec![Keystroke::simple(keycode::ENTER)]);
231
+ continue;
232
+ }
233
+ if character == '\t' {
234
+ plan.push(vec![Keystroke::simple(keycode::TAB)]);
235
+ continue;
236
+ }
237
+ match self.keystroke_for_char(character) {
238
+ Some(keystroke) => plan.push(vec![keystroke]),
239
+ None => unreachable.push(character),
240
+ }
241
+ }
242
+ (plan, unreachable)
243
+ }
244
+ }
245
+
246
+ #[cfg(test)]
247
+ mod tests {
248
+ use super::*;
249
+
250
+ #[test]
251
+ fn a_layout_that_comes_from_the_environment_is_the_one_compiled() {
252
+ let names = LayoutNames::from_lookup(|key| match key {
253
+ "XKB_DEFAULT_LAYOUT" => Some(String::from("de")),
254
+ _ => None,
255
+ });
256
+ assert_eq!(names.identity(), "de");
257
+
258
+ let layout = Layout::from_names(&names).expect("the German keymap should compile");
259
+ let (plan, unreachable) = layout.plan_text("z");
260
+ assert!(
261
+ unreachable.is_empty(),
262
+ "z should be reachable: {unreachable:?}"
263
+ );
264
+ // QWERTZ puts z where a US layout puts y, so the compiled keymap is the
265
+ // environment's, not the library default.
266
+ assert_eq!(plan[0][0].code, 21, "z must sit on the y key position");
267
+ }
268
+
269
+ #[test]
270
+ fn a_session_with_no_layout_variable_reports_the_library_default() {
271
+ assert_eq!(LayoutNames::from_lookup(|_| None).identity(), "us");
272
+ }
273
+
274
+ #[test]
275
+ fn a_character_is_planned_as_a_real_key_event_on_the_compiled_layout() {
276
+ let layout = Layout::from_names(&LayoutNames::from_lookup(|_| None))
277
+ .expect("a keymap should compile");
278
+ let (plan, unreachable) = layout.plan_text("aA.\n");
279
+ assert!(
280
+ unreachable.is_empty(),
281
+ "ASCII should be reachable: {unreachable:?}"
282
+ );
283
+ assert_eq!(plan.len(), 4);
284
+ assert!(!plan[0][0].shift, "lower-case a needs no shift");
285
+ assert!(
286
+ plan[1][0].shift,
287
+ "upper-case A needs shift on any normal layout"
288
+ );
289
+ assert_eq!(plan[0][0].code, plan[1][0].code, "both letters share a key");
290
+ // The plan is evdev, but the real injector expects Moonlight/VK codes.
291
+ // Passing Linux N (49) straight through instead presses VK_1.
292
+ let native: Vec<_> = plan
293
+ .iter()
294
+ .flatten()
295
+ .map(|stroke| crate::input::native_keycode(stroke.code).unwrap())
296
+ .collect();
297
+ assert_eq!(native, [0x41, 0x41, 0xBE, 0x0D]);
298
+ assert_eq!(
299
+ crate::input::native_keycode(plan[1][0].modifiers()[0]).unwrap(),
300
+ 0x10
301
+ );
302
+ assert_eq!(
303
+ crate::input::native_keycode(modifier_key("Control").unwrap()).unwrap(),
304
+ 0x11
305
+ );
306
+ }
307
+
308
+ #[test]
309
+ fn named_keys_and_modifiers_resolve_to_linux_key_codes() {
310
+ assert_eq!(named_key("Enter").unwrap().code, keycode::ENTER);
311
+ assert_eq!(named_key("ArrowLeft").unwrap().code, keycode::LEFT);
312
+ assert_eq!(modifier_key("Control"), Some(keycode::LEFT_CTRL));
313
+ assert!(named_key("Frobnicate").is_none());
314
+ assert!(modifier_key("Hyper").is_none());
315
+ }
316
+
317
+ #[test]
318
+ fn text_this_layout_cannot_produce_is_reported_not_invented() {
319
+ let layout = Layout::from_environment().unwrap();
320
+ let (_, unreachable) = layout.plan_text("\u{1F600}");
321
+ assert_eq!(unreachable, "\u{1F600}");
322
+ }
323
+
324
+ #[test]
325
+ fn a_symbol_only_reachable_beyond_four_levels_is_refused() {
326
+ let names = LayoutNames::from_lookup(|key| match key {
327
+ "XKB_DEFAULT_LAYOUT" => Some(String::from("de")),
328
+ "XKB_DEFAULT_VARIANT" => Some(String::from("neo")),
329
+ _ => None,
330
+ });
331
+ let layout = Layout::from_names(&names).expect("the Neo keymap should compile");
332
+ assert!(
333
+ layout.keystroke_for_char('\u{a3}').is_none(),
334
+ "the pound sign sits on a level this engine cannot press"
335
+ );
336
+ assert!(layout.keystroke_for_char('6').is_some());
337
+ }
338
+ }