use std::{collections::HashSet, fmt::Display, task::Poll}; use async_trait::async_trait; use futures::StreamExt; use futures_core::Stream; use input_event::{scancode, Event, KeyboardEvent}; pub use error::{CaptureCreationError, CaptureError, InputCaptureError}; pub mod error; #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] mod libei; #[cfg(target_os = "macos")] mod macos; #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] mod wayland; #[cfg(windows)] mod windows; #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] mod x11; /// fallback input capture (does not produce events) mod dummy; pub type CaptureHandle = u64; #[derive(Copy, Clone, Debug, PartialEq)] pub enum CaptureEvent { /// capture on this capture handle is now active Begin, /// input event coming from capture handle Input(Event), } impl Display for CaptureEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CaptureEvent::Begin => write!(f, "begin capture"), CaptureEvent::Input(e) => write!(f, "{e}"), } } } #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum Position { Left, Right, Top, Bottom, } impl Position { pub fn opposite(&self) -> Self { match self { Position::Left => Self::Right, Position::Right => Self::Left, Position::Top => Self::Bottom, Position::Bottom => Self::Top, } } } impl Display for Position { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let pos = match self { Position::Left => "left", Position::Right => "right", Position::Top => "top", Position::Bottom => "bottom", }; write!(f, "{}", pos) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Backend { #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] InputCapturePortal, #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] LayerShell, #[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 = "libei", not(target_os = "macos")))] Backend::InputCapturePortal => write!(f, "input-capture-portal"), #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] Backend::LayerShell => write!(f, "layer-shell"), #[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 InputCapture { capture: Box, pressed_keys: HashSet, } impl InputCapture { /// create a new client with the given id pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> { self.capture.create(id, pos).await } /// destroy the client with the given id, if it exists pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> { self.capture.destroy(id).await } /// release mouse pub async fn release(&mut self) -> Result<(), CaptureError> { self.pressed_keys.clear(); self.capture.release().await } /// destroy the input capture pub async fn terminate(&mut self) -> Result<(), CaptureError> { self.capture.terminate().await } /// creates a new [`InputCapture`] pub async fn new(backend: Option) -> Result { let capture = create(backend).await?; Ok(Self { capture, pressed_keys: HashSet::new(), }) } /// check whether the given keys are pressed pub fn keys_pressed(&self, keys: &[scancode::Linux]) -> bool { keys.iter().all(|k| self.pressed_keys.contains(k)) } fn update_pressed_keys(&mut self, key: u32, state: u8) { if let Ok(scancode) = scancode::Linux::try_from(key) { log::debug!("key: {key}, state: {state}, scancode: {scancode:?}"); match state { 1 => self.pressed_keys.insert(scancode), _ => self.pressed_keys.remove(&scancode), }; } } } impl Stream for InputCapture { type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; fn poll_next( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { match self.capture.poll_next_unpin(cx) { Poll::Ready(e) => { if let Some(Ok(( _, CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key, state, .. })), ))) = e { self.update_pressed_keys(key, state); } Poll::Ready(e) } Poll::Pending => Poll::Pending, } } } #[async_trait] trait Capture: Stream> + Unpin { /// create a new client with the given id async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError>; /// destroy the client with the given id, if it exists async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError>; /// release mouse async fn release(&mut self) -> Result<(), CaptureError>; /// destroy the input capture async fn terminate(&mut self) -> Result<(), CaptureError>; } async fn create_backend( backend: Backend, ) -> Result< Box>>, CaptureCreationError, > { match backend { #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)), #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] Backend::LayerShell => Ok(Box::new(wayland::WaylandInputCapture::new()?)), #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)), #[cfg(windows)] Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())), #[cfg(target_os = "macos")] Backend::MacOs => Ok(Box::new(macos::MacOSInputCapture::new().await?)), Backend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())), } } async fn create( backend: Option, ) -> Result< Box>>, CaptureCreationError, > { if let Some(backend) = backend { let b = create_backend(backend).await; if b.is_ok() { log::info!("using capture backend: {backend}"); } return b; } for backend in [ #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] Backend::InputCapturePortal, #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] Backend::LayerShell, #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] Backend::X11, #[cfg(windows)] Backend::Windows, #[cfg(target_os = "macos")] Backend::MacOs, ] { match create_backend(backend).await { Ok(b) => { log::info!("using capture backend: {backend}"); return Ok(b); } Err(e) if e.cancelled_by_user() => return Err(e), Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"), } } Err(CaptureCreationError::NoAvailableBackend) }