Files
lan-mouse/input-capture/src/lib.rs
2024-11-07 12:43:42 +01:00

339 lines
10 KiB
Rust

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 = "layer_shell", not(target_os = "macos")))]
mod layer_shell;
#[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 = "layer_shell", 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 = "layer_shell", 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<dyn Capture>,
/// keys pressed by active capture
pressed_keys: HashSet<scancode::Linux>,
/// map from position to ids
position_map: HashMap<Position, Vec<CaptureHandle>>,
/// map from id to position
id_map: HashMap<CaptureHandle, Position>,
/// 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> {
assert!(!self.id_map.contains_key(&id));
self.id_map.insert(id, pos);
if let Some(v) = self.position_map.get_mut(&pos) {
v.push(id);
Ok(())
} else {
self.position_map.insert(pos, vec![id]);
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> {
let pos = self
.id_map
.remove(&id)
.expect("no position for this handle");
log::debug!("destroying capture {id} @ {pos}");
let remaining = self.position_map.get_mut(&pos).expect("id vector");
remaining.retain(|&i| i != id);
log::debug!("remaining ids @ {pos}: {remaining:?}");
if remaining.is_empty() {
log::debug!("destroying capture @ {pos} - no remaining ids");
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<Backend>) -> Result<Self, CaptureCreationError> {
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<Option<Self::Item>> {
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<Item = Result<(Position, CaptureEvent), CaptureError>> + 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<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
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 = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(layer_shell::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<Backend>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
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 = "layer_shell", 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)
}