use async_trait::async_trait; use std::{ collections::{HashMap, HashSet}, fmt::Display, }; use input_event::{Event, KeyboardEvent}; pub use self::error::{EmulationCreationError, EmulationError, InputEmulationError}; #[cfg(windows)] mod windows; #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] mod x11; #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] mod wlroots; #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))] mod xdg_desktop_portal; #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] mod libei; #[cfg(target_os = "macos")] mod macos; /// fallback input emulation (logs events) mod dummy; mod error; pub type EmulationHandle = u64; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Backend { #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] Wlroots, #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Libei, #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))] Xdp, #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] X11, #[cfg(windows)] Windows, #[cfg(target_os = "macos")] MacOs, Dummy, } impl Display for Backend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] Backend::Wlroots => write!(f, "wlroots"), #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Backend::Libei => write!(f, "libei"), #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))] Backend::Xdp => write!(f, "xdg-desktop-portal"), #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] Backend::X11 => write!(f, "X11"), #[cfg(windows)] Backend::Windows => write!(f, "windows"), #[cfg(target_os = "macos")] Backend::MacOs => write!(f, "macos"), Backend::Dummy => write!(f, "dummy"), } } } pub struct InputEmulation { emulation: Box, handles: HashSet, pressed_keys: HashMap>, } impl InputEmulation { async fn with_backend(backend: Backend) -> Result { let emulation: Box = match backend { #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?), #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Backend::Libei => Box::new(libei::LibeiEmulation::new().await?), #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] Backend::X11 => Box::new(x11::X11Emulation::new()?), #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))] Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?), #[cfg(windows)] Backend::Windows => Box::new(windows::WindowsEmulation::new()?), #[cfg(target_os = "macos")] Backend::MacOs => Box::new(macos::MacOSEmulation::new()?), Backend::Dummy => Box::new(dummy::DummyEmulation::new()), }; Ok(Self { emulation, handles: HashSet::new(), pressed_keys: HashMap::new(), }) } pub async fn new(backend: Option) -> Result { if let Some(backend) = backend { let b = Self::with_backend(backend).await; if b.is_ok() { log::info!("using emulation backend: {backend}"); } return b; } for backend in [ #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] Backend::Wlroots, #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Backend::Libei, #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))] Backend::Xdp, #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] Backend::X11, #[cfg(windows)] Backend::Windows, #[cfg(target_os = "macos")] Backend::MacOs, Backend::Dummy, ] { match Self::with_backend(backend).await { Ok(b) => { log::info!("using emulation backend: {backend}"); return Ok(b); } Err(e) if e.cancelled_by_user() => return Err(e), Err(e) => log::warn!("{e}"), } } Err(EmulationCreationError::NoAvailableBackend) } pub async fn consume( &mut self, event: Event, handle: EmulationHandle, ) -> Result<(), EmulationError> { match event { Event::Keyboard(KeyboardEvent::Key { key, state, .. }) => { // prevent double pressed / released keys if self.update_pressed_keys(handle, key, state) { self.emulation.consume(event, handle).await?; } Ok(()) } _ => self.emulation.consume(event, handle).await, } } pub async fn create(&mut self, handle: EmulationHandle) -> bool { if self.handles.insert(handle) { self.pressed_keys.insert(handle, HashSet::new()); self.emulation.create(handle).await; true } else { false } } pub async fn destroy(&mut self, handle: EmulationHandle) { let _ = self.release_keys(handle).await; if self.handles.remove(&handle) { self.pressed_keys.remove(&handle); self.emulation.destroy(handle).await } } pub async fn terminate(&mut self) { for handle in self.handles.iter().cloned().collect::>() { self.destroy(handle).await } self.emulation.terminate().await } pub async fn release_keys(&mut self, handle: EmulationHandle) -> Result<(), EmulationError> { if let Some(keys) = self.pressed_keys.get_mut(&handle) { let keys = keys.drain().collect::>(); for key in keys { let event = Event::Keyboard(KeyboardEvent::Key { time: 0, key, state: 0, }); self.emulation.consume(event, handle).await?; if let Ok(key) = input_event::scancode::Linux::try_from(key) { log::warn!("releasing stuck key: {key:?}"); } } } let event = Event::Keyboard(KeyboardEvent::Modifiers { depressed: 0, latched: 0, locked: 0, group: 0, }); self.emulation.consume(event, handle).await?; Ok(()) } pub fn has_pressed_keys(&self, handle: EmulationHandle) -> bool { self.pressed_keys .get(&handle) .is_some_and(|p| !p.is_empty()) } /// update the pressed_keys for the given handle /// returns whether the event should be processed fn update_pressed_keys(&mut self, handle: EmulationHandle, key: u32, state: u8) -> bool { let Some(pressed_keys) = self.pressed_keys.get_mut(&handle) else { return false; }; if state == 0 { // currently pressed => can release pressed_keys.remove(&key) } else { // currently not pressed => can press pressed_keys.insert(key) } } } #[async_trait] trait Emulation: Send { async fn consume( &mut self, event: Event, handle: EmulationHandle, ) -> Result<(), EmulationError>; async fn create(&mut self, handle: EmulationHandle); async fn destroy(&mut self, handle: EmulationHandle); async fn terminate(&mut self); }