use std::sync::Mutex;
use evdev::uinput::VirtualDevice;
use evdev::{
AbsInfo, AbsoluteAxisCode, AttributeSet, BusType, EventType, InputEvent, InputId, KeyCode,
PropType, RelativeAxisCode, UinputAbsSetup,
};
use xkbcommon::xkb;
use xa11y_core::input::{Key as XaKey, MouseButton, Point, ScrollDelta};
use xa11y_core::{Error, Result};
use crate::input::{char_keysym, key_to_keysym, XK_SHIFT_L};
const COORD_MAX: i32 = 32767;
const DEFAULT_SCREEN_W: i32 = 1920;
const DEFAULT_SCREEN_H: i32 = 1080;
pub(crate) struct WaylandInputBackend {
inner: Mutex<UinputState>,
screen_w: i32,
screen_h: i32,
}
struct UinputState {
device: VirtualDevice,
keymap: ReverseKeymap,
}
struct ReverseKeymap {
table: std::collections::HashMap<u32, (u16, bool)>,
}
impl ReverseKeymap {
fn from_xkb(keymap: &xkb::Keymap) -> Self {
let mut table = std::collections::HashMap::new();
let min: u32 = keymap.min_keycode().into();
let max: u32 = keymap.max_keycode().into();
for kc_raw in min..=max {
let kc_xkb = xkb::Keycode::from(kc_raw);
for level in 0..2u32 {
for sym in keymap.key_get_syms_by_level(kc_xkb, 0, level) {
let raw: u32 = (*sym).into();
if raw == 0 {
continue;
}
let evdev_kc = kc_raw.wrapping_sub(8);
if evdev_kc > u16::MAX as u32 {
continue;
}
table.entry(raw).or_insert((evdev_kc as u16, level == 1));
}
}
}
Self { table }
}
fn lookup(&self, keysym: u32) -> Option<(u16, bool)> {
self.table.get(&keysym).copied()
}
}
impl WaylandInputBackend {
pub(crate) fn new() -> Result<Self> {
let (screen_w, screen_h) = detect_screen_size();
let xkb_ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let keymap = xkb::Keymap::new_from_names(
&xkb_ctx,
"",
"",
"",
"",
None,
xkb::KEYMAP_COMPILE_NO_FLAGS,
)
.ok_or_else(|| Error::Platform {
code: -1,
message: "xkbcommon: failed to compile default keymap".into(),
})?;
let reverse = ReverseKeymap::from_xkb(&keymap);
let mut keys = AttributeSet::<KeyCode>::new();
for &(evdev_kc, _shift) in reverse.table.values() {
keys.insert(KeyCode(evdev_kc));
}
for btn in [KeyCode::BTN_LEFT, KeyCode::BTN_RIGHT, KeyCode::BTN_MIDDLE] {
keys.insert(btn);
}
let abs_x = UinputAbsSetup::new(
AbsoluteAxisCode::ABS_X,
AbsInfo::new(0, 0, COORD_MAX, 0, 0, 1),
);
let abs_y = UinputAbsSetup::new(
AbsoluteAxisCode::ABS_Y,
AbsInfo::new(0, 0, COORD_MAX, 0, 0, 1),
);
let mut props = AttributeSet::<PropType>::new();
props.insert(PropType::POINTER);
let mut rels = AttributeSet::<RelativeAxisCode>::new();
rels.insert(RelativeAxisCode::REL_WHEEL);
rels.insert(RelativeAxisCode::REL_HWHEEL);
let device = VirtualDevice::builder()
.map_err(map_open_err)?
.name("xa11y virtual input")
.input_id(InputId::new(BusType::BUS_VIRTUAL, 0x1209, 0xa11a, 1))
.with_keys(&keys)
.map_err(map_io)?
.with_absolute_axis(&abs_x)
.map_err(map_io)?
.with_absolute_axis(&abs_y)
.map_err(map_io)?
.with_relative_axes(&rels)
.map_err(map_io)?
.with_properties(&props)
.map_err(map_io)?
.build()
.map_err(map_io)?;
Ok(Self {
inner: Mutex::new(UinputState {
device,
keymap: reverse,
}),
screen_w,
screen_h,
})
}
fn map_x(&self, x: i32) -> i32 {
let w = if self.screen_w > 0 { self.screen_w } else { 1 };
((x as i64) * (COORD_MAX as i64) / (w as i64)).clamp(0, COORD_MAX as i64) as i32
}
fn map_y(&self, y: i32) -> i32 {
let h = if self.screen_h > 0 { self.screen_h } else { 1 };
((y as i64) * (COORD_MAX as i64) / (h as i64)).clamp(0, COORD_MAX as i64) as i32
}
fn lock(&self) -> std::sync::MutexGuard<'_, UinputState> {
self.inner.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn pointer_move(&self, to: Point) -> Result<()> {
let x = self.map_x(to.x);
let y = self.map_y(to.y);
let mut s = self.lock();
emit(
&mut s.device,
&[
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, x),
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, y),
],
)
}
pub(crate) fn pointer_down(&self, button: MouseButton) -> Result<()> {
let key = button_key(button);
let mut s = self.lock();
emit(
&mut s.device,
&[InputEvent::new(EventType::KEY.0, key.0, 1)],
)
}
pub(crate) fn pointer_up(&self, button: MouseButton) -> Result<()> {
let key = button_key(button);
let mut s = self.lock();
emit(
&mut s.device,
&[InputEvent::new(EventType::KEY.0, key.0, 0)],
)
}
pub(crate) fn pointer_click(&self, at: Point, button: MouseButton, count: u32) -> Result<()> {
if count == 0 {
return Ok(());
}
let x = self.map_x(at.x);
let y = self.map_y(at.y);
let key = button_key(button);
let mut s = self.lock();
let mut frame = vec![
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, x),
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, y),
];
for _ in 0..count {
frame.push(InputEvent::new(EventType::KEY.0, key.0, 1));
frame.push(InputEvent::new(EventType::KEY.0, key.0, 0));
}
emit(&mut s.device, &frame)
}
pub(crate) fn pointer_scroll(&self, at: Point, delta: ScrollDelta) -> Result<()> {
let x = self.map_x(at.x);
let y = self.map_y(at.y);
let mut s = self.lock();
let wheel = -delta.dy;
let hwheel = delta.dx;
let mut frame = vec![
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, x),
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, y),
];
if wheel != 0 {
frame.push(InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_WHEEL.0,
wheel,
));
}
if hwheel != 0 {
frame.push(InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_HWHEEL.0,
hwheel,
));
}
emit(&mut s.device, &frame)
}
pub(crate) fn key_down(&self, key: &XaKey) -> Result<()> {
let keysym = key_to_keysym(key)?;
let mut s = self.lock();
let (kc, _shift) = s.keymap.lookup(keysym).ok_or_else(|| Error::Unsupported {
feature: format!("keysym 0x{keysym:04x} not in xkb keymap"),
})?;
emit(&mut s.device, &[InputEvent::new(EventType::KEY.0, kc, 1)])
}
pub(crate) fn key_up(&self, key: &XaKey) -> Result<()> {
let keysym = key_to_keysym(key)?;
let mut s = self.lock();
let (kc, _shift) = s.keymap.lookup(keysym).ok_or_else(|| Error::Unsupported {
feature: format!("keysym 0x{keysym:04x} not in xkb keymap"),
})?;
emit(&mut s.device, &[InputEvent::new(EventType::KEY.0, kc, 0)])
}
pub(crate) fn type_text(&self, text: &str) -> Result<()> {
let mut s = self.lock();
let shift_kc = s
.keymap
.lookup(XK_SHIFT_L)
.ok_or_else(|| Error::Unsupported {
feature: "no Shift_L in xkb keymap".into(),
})?
.0;
for c in text.chars() {
let keysym = char_keysym(c);
let (kc, needs_shift) = s.keymap.lookup(keysym).ok_or_else(|| Error::Unsupported {
feature: format!(
"character '{c}' (keysym 0x{keysym:04x}) has no keycode \
in the current xkb keyboard layout"
),
})?;
let mut frame = vec![];
if needs_shift {
frame.push(InputEvent::new(EventType::KEY.0, shift_kc, 1));
}
frame.push(InputEvent::new(EventType::KEY.0, kc, 1));
frame.push(InputEvent::new(EventType::KEY.0, kc, 0));
if needs_shift {
frame.push(InputEvent::new(EventType::KEY.0, shift_kc, 0));
}
emit(&mut s.device, &frame)?;
}
Ok(())
}
}
fn emit(dev: &mut VirtualDevice, events: &[InputEvent]) -> Result<()> {
dev.emit(events).map_err(map_io)
}
fn button_key(b: MouseButton) -> KeyCode {
match b {
MouseButton::Left => KeyCode::BTN_LEFT,
MouseButton::Right => KeyCode::BTN_RIGHT,
MouseButton::Middle => KeyCode::BTN_MIDDLE,
}
}
fn map_open_err(e: std::io::Error) -> Error {
use std::io::ErrorKind;
match e.kind() {
ErrorKind::PermissionDenied => Error::PermissionDenied {
instructions: "open /dev/uinput: permission denied — add the user to the `input` \
group (sudo usermod -aG input $USER) and re-login, then re-run."
.into(),
},
ErrorKind::NotFound => Error::Unsupported {
feature: "/dev/uinput not present — kernel uinput module not loaded \
(try `sudo modprobe uinput`)"
.into(),
},
_ => Error::Platform {
code: e.raw_os_error().unwrap_or(-1) as i64,
message: format!("uinput open: {e}"),
},
}
}
fn map_io(e: std::io::Error) -> Error {
Error::Platform {
code: e.raw_os_error().unwrap_or(-1) as i64,
message: format!("uinput: {e}"),
}
}
fn detect_screen_size() -> (i32, i32) {
let parse = |name: &str, default: i32| -> i32 {
std::env::var(name)
.ok()
.and_then(|s| s.parse::<i32>().ok())
.filter(|n| *n > 0)
.unwrap_or(default)
};
(
parse("XA11Y_SCREEN_WIDTH", DEFAULT_SCREEN_W),
parse("XA11Y_SCREEN_HEIGHT", DEFAULT_SCREEN_H),
)
}
#[cfg(test)]
mod tests {
use super::*;
use xa11y_core::input::Key as XaKey;
fn default_keymap() -> xkb::Keymap {
let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
xkb::Keymap::new_from_names(&ctx, "", "", "us", "", None, xkb::KEYMAP_COMPILE_NO_FLAGS)
.expect("US keymap should compile from xkbcommon defaults")
}
#[test]
fn reverse_keymap_maps_lowercase_letter() {
let reverse = ReverseKeymap::from_xkb(&default_keymap());
let (_kc, needs_shift) = reverse.lookup(char_keysym('a')).expect("'a' in US keymap");
assert!(!needs_shift, "'a' should be on the unshifted level");
}
#[test]
fn reverse_keymap_maps_uppercase_letter_to_same_keycode_with_shift() {
let reverse = ReverseKeymap::from_xkb(&default_keymap());
let lower = reverse.lookup(char_keysym('a')).expect("'a'");
let upper = reverse.lookup(char_keysym('A')).expect("'A'");
assert_eq!(lower.0, upper.0, "'a' and 'A' must share a keycode");
assert!(!lower.1);
assert!(upper.1, "'A' must require shift");
}
#[test]
fn reverse_keymap_resolves_named_keys() {
let reverse = ReverseKeymap::from_xkb(&default_keymap());
for key in [
XaKey::Enter,
XaKey::Escape,
XaKey::Tab,
XaKey::Backspace,
XaKey::Space,
] {
let sym = key_to_keysym(&key).expect("named keys map to keysyms");
assert!(
reverse.lookup(sym).is_some(),
"{key:?} (keysym 0x{sym:04x}) missing from US keymap"
);
}
}
#[test]
fn reverse_keymap_resolves_shift_l() {
let reverse = ReverseKeymap::from_xkb(&default_keymap());
assert!(
reverse.lookup(XK_SHIFT_L).is_some(),
"Shift_L must be in any keymap"
);
}
#[test]
fn button_key_uses_input_event_codes() {
assert_eq!(button_key(MouseButton::Left), KeyCode::BTN_LEFT);
assert_eq!(button_key(MouseButton::Right), KeyCode::BTN_RIGHT);
assert_eq!(button_key(MouseButton::Middle), KeyCode::BTN_MIDDLE);
}
#[test]
fn screen_size_respects_env_override() {
unsafe {
std::env::set_var("XA11Y_SCREEN_WIDTH", "2560");
std::env::set_var("XA11Y_SCREEN_HEIGHT", "1440");
}
let (w, h) = detect_screen_size();
unsafe {
std::env::remove_var("XA11Y_SCREEN_WIDTH");
std::env::remove_var("XA11Y_SCREEN_HEIGHT");
}
assert_eq!(w, 2560);
assert_eq!(h, 1440);
}
}