@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.
- package/LICENSE +202 -0
- package/NOTICE +44 -0
- package/README.md +266 -0
- package/bin/desklink-host.mjs +113 -0
- package/dist/bridge.d.ts +57 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +208 -0
- package/dist/bridge.js.map +1 -0
- package/dist/engineProcess.d.ts +82 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +243 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +169 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resolveEngine.d.ts +34 -0
- package/dist/resolveEngine.d.ts.map +1 -0
- package/dist/resolveEngine.js +103 -0
- package/dist/resolveEngine.js.map +1 -0
- package/docs/BOUNDARY.md +145 -0
- package/docs/PROTOCOL.md +363 -0
- package/engine/Cargo.lock +3077 -0
- package/engine/Cargo.toml +37 -0
- package/engine/build.rs +107 -0
- package/engine/native/inputtino_shim.cpp +11 -0
- package/engine/native/vpx_shim.c +184 -0
- package/engine/native/wrapper.h +7 -0
- package/engine/src/capture.rs +510 -0
- package/engine/src/clipboard.rs +132 -0
- package/engine/src/convert.rs +299 -0
- package/engine/src/encoder.rs +204 -0
- package/engine/src/input.rs +355 -0
- package/engine/src/keymap.rs +338 -0
- package/engine/src/main.rs +614 -0
- package/engine/src/peer.rs +775 -0
- package/engine/src/portal.rs +115 -0
- package/engine/src/protocol.rs +331 -0
- package/engine/src/session.rs +2159 -0
- package/engine/src/x11.rs +244 -0
- package/engine/vendor/inputtino/.clang-format +166 -0
- package/engine/vendor/inputtino/.clang-tidy +30 -0
- package/engine/vendor/inputtino/.dockerignore +11 -0
- package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
- package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
- package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
- package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
- package/engine/vendor/inputtino/CMakeLists.txt +187 -0
- package/engine/vendor/inputtino/LICENSE +21 -0
- package/engine/vendor/inputtino/README.md +110 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
- package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
- package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
- package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
- package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
- package/engine/vendor/inputtino/bindings/python/README.md +215 -0
- package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
- package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
- package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
- package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
- package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
- package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
- package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
- package/engine/vendor/inputtino/docker/Dockerfile +70 -0
- package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
- package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
- package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
- package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
- package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
- package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
- package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
- package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
- package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
- package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
- package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
- package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
- package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
- package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
- package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
- package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
- package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
- package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
- package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
- package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
- package/examples/reference-client.html +428 -0
- 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
|
+
}
|