mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-07 20:09:59 +03:00
263 lines
8.0 KiB
Rust
263 lines
8.0 KiB
Rust
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<dyn Capture>,
|
|
pressed_keys: HashSet<scancode::Linux>,
|
|
}
|
|
|
|
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<Backend>) -> Result<Self, CaptureCreationError> {
|
|
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),
|
|
};
|
|
}
|
|
log::info!("pressed keys: {:?}", self.pressed_keys);
|
|
}
|
|
}
|
|
|
|
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>> {
|
|
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<Item = Result<(CaptureHandle, CaptureEvent), CaptureError>> + 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<dyn Capture<Item = Result<(CaptureHandle, 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 = "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<Backend>,
|
|
) -> Result<
|
|
Box<dyn Capture<Item = Result<(CaptureHandle, 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 = "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)
|
|
}
|