mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-08 04:20:01 +03:00
339 lines
10 KiB
Rust
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)
|
|
}
|