use std::{ collections::{HashMap, HashSet, VecDeque}, fmt::Display, mem::swap, task::{ready, 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 backend capture: Box, /// keys pressed by active capture pressed_keys: HashSet, /// map from position to ids position_map: HashMap>, /// map from id to position id_map: HashMap, /// pending events pending: VecDeque<(CaptureHandle, CaptureEvent)>, } impl InputCapture { /// create a new client with the given id pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> { if let Some(v) = self.position_map.get_mut(&pos) { v.push(id); Ok(()) } else { self.position_map.insert(pos, vec![id]); self.id_map.insert(id, pos); self.capture.create(pos).await } } /// destroy the client with the given id, if it exists pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> { if let Some(pos) = self.id_map.remove(&id) { let destroy = if let Some(v) = self.position_map.get_mut(&pos) { v.retain(|&i| i != id); // we were the last id registered at this position v.is_empty() } else { // nothing to destroy false }; if destroy { self.position_map.remove(&pos); self.capture.destroy(pos).await?; } } Ok(()) } /// 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, id_map: Default::default(), pending: Default::default(), position_map: Default::default(), 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> { if let Some(e) = self.pending.pop_front() { return Poll::Ready(Some(Ok(e))); } // ready let event = ready!(self.capture.poll_next_unpin(cx)); // stream closed let event = match event { Some(e) => e, None => return Poll::Ready(None), }; // error occurred let (pos, event) = match event { Ok(e) => e, Err(e) => return Poll::Ready(Some(Err(e))), }; // handle key presses if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key, state, .. })) = event { self.update_pressed_keys(key, state); } let len = self .position_map .get(&pos) .map(|ids| ids.len()) .unwrap_or(0); match len { 0 => Poll::Pending, 1 => Poll::Ready(Some(Ok(( self.position_map.get(&pos).expect("no id")[0], event, )))), _ => { let mut position_map = HashMap::new(); swap(&mut self.position_map, &mut position_map); { for &id in position_map.get(&pos).expect("position") { self.pending.push_back((id, event)); } } swap(&mut self.position_map, &mut position_map); Poll::Ready(Some(Ok(self.pending.pop_front().expect("event")))) } } } } #[async_trait] trait Capture: Stream> + Unpin { /// create a new client with the given id async fn create(&mut self, pos: Position) -> Result<(), CaptureError>; /// destroy the client with the given id, if it exists async fn destroy(&mut self, pos: Position) -> 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::LayerShellInputCapture::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) }