Compare commits

...

42 Commits

Author SHA1 Message Date
Ferdinand Schober
e45df5d20e remove anyhow dependency from input-emulation 2024-07-10 00:21:46 +02:00
Ferdinand Schober
b14dcb6f49 simplify 2024-07-09 20:39:08 +02:00
Ferdinand Schober
289c21e000 fix 2024-07-09 20:16:48 +02:00
Ferdinand Schober
cb6ad7f1ac cleanup 2024-07-09 20:09:28 +02:00
Ferdinand Schober
3933b6bf24 use thiserror macro 2024-07-09 19:07:36 +02:00
Ferdinand Schober
65303684f4 simplify 2024-07-09 19:00:17 +02:00
Ferdinand Schober
eaa3d9d5b3 fix windows + rename to dx/dy everywhere 2024-07-09 18:58:01 +02:00
Ferdinand Schober
306e0e6a60 fix macos 2024-07-09 18:43:57 +02:00
Ferdinand Schober
0d14619172 more cleanup 2024-07-09 17:24:27 +02:00
Ferdinand Schober
09b2db50c0 code cleanup 2024-07-09 17:24:27 +02:00
Ferdinand Schober
703465a370 fix drop impl for desktop-portal 2024-07-05 01:41:11 +02:00
Ferdinand Schober
ef3ebc59bd adjust error handling 2024-07-05 01:41:11 +02:00
Ferdinand Schober
37a8f729ea remove dispatch workaround 2024-07-05 01:41:11 +02:00
Ferdinand Schober
9abec63313 update dependencies 2024-07-03 11:11:50 +02:00
Ferdinand Schober
35e626976e add explicit version 2024-07-02 22:39:20 +02:00
Ferdinand Schober
684735b499 fix dependencies in input-event 2024-07-02 22:34:43 +02:00
Ferdinand Schober
abfc744e4c specify dependency versions explicitly 2024-07-02 22:17:26 +02:00
Ferdinand Schober
fb2c39e8ae fix all features enabled by default 2024-07-02 22:07:37 +02:00
Ferdinand Schober
82ab5ecbbd fix xdg-desktop-portal backend not available 2024-07-02 22:07:37 +02:00
Ferdinand Schober
3fd2b31562 update deps 2024-07-02 22:07:37 +02:00
Ferdinand Schober
90e83cee87 purge dependencies 2024-07-02 22:07:37 +02:00
Ferdinand Schober
4db2d37f32 split into input-{event,capture,emulation} 2024-07-02 22:07:37 +02:00
Ferdinand Schober
7b511bb97d fix iteration order 2024-07-02 16:23:07 +02:00
Ferdinand Schober
70a23b9fa7 reduce coupling of emulation and capture backends 2024-07-02 16:23:07 +02:00
Ferdinand Schober
b6b16063a8 Configurable emulation backend (#151) 2024-07-01 20:09:16 +02:00
Ferdinand Schober
9cbe1ed8d8 fix release bind message 2024-06-30 14:41:16 +02:00
Ferdinand Schober
3528ef4fae Configurable capture backend (#150)
capture backend can now be configured via the `capture_backend` cli argument / config entry
2024-06-29 00:10:36 +02:00
Ferdinand Schober
232c048c19 fix transmuting to pointer types UB (#147)
closes #134
2024-06-25 14:13:05 +02:00
Orhun Parmaksız
1c37579ae5 Update Arch Linux instructions (#145) 2024-06-19 20:28:30 +02:00
Ferdinand Schober
460bacade5 fix sizeof usize assumed to be 8 (#143)
closes #141
2024-06-09 00:49:00 +02:00
虢豳
5fd3b719d6 Extract package name and version from Cargo.toml (#136)
* chore: nix flake update

* feat: Extract package name and version from Cargo.toml
2024-05-22 08:06:44 +02:00
Ferdinand Schober
e6d4585bb2 chore: Release lan-mouse version 0.8.0 2024-05-17 18:06:11 +02:00
Ferdinand Schober
1c082d5c0c fix win -> lin scancode translations for intl keys 2024-05-17 17:39:52 +02:00
Ferdinand Schober
cd98acbd08 fix right shift not working 2024-05-16 22:36:51 +02:00
Ferdinand Schober
11e1919588 fix most international keybindings 2024-05-16 22:29:41 +02:00
Ferdinand Schober
152bceaa86 fix dns resolving 2024-05-13 08:12:55 +02:00
Ferdinand Schober
60180d841c fix formatting 2024-05-12 15:50:03 +02:00
Ferdinand Schober
5802a0be0b fix discrete scrolling on wlroots 2024-05-12 15:49:39 +02:00
Ferdinand Schober
1737727d61 fix cast 2024-05-12 15:07:29 +02:00
Ferdinand Schober
ba46037a1f fix scrolling in windows 2024-05-12 14:55:56 +02:00
Ferdinand Schober
da768b6fb8 ignore Axis events corresponding to v120 events 2024-05-12 13:32:14 +02:00
Ferdinand Schober
799b45104a enter hook command (#130)
new configuration option `enter_hook` can now be used to spawn a command when a client is entered
2024-05-12 13:01:07 +02:00
51 changed files with 3164 additions and 2586 deletions

886
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +1,55 @@
[workspace]
members = [
"input-capture",
"input-emulation",
"input-event",
]
[package] [package]
name = "lan-mouse" name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks" description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.7.3" version = "0.8.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse" repository = "https://github.com/ferdinandschober/lan-mouse"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.release] [profile.release]
strip = true strip = true
lto = "fat" lto = "fat"
[dependencies] [dependencies]
tempfile = "3.8" input-event = { path = "input-event", version = "0.1.0" }
input-emulation = { path = "input-emulation", version = "0.1.0", default-features = false }
input-capture = { path = "input-capture", version = "0.1.0", default-features = false }
hickory-resolver = "0.24.1" hickory-resolver = "0.24.1"
memmap = "0.7"
toml = "0.8" toml = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71" anyhow = "1.0.71"
log = "0.4.20" log = "0.4.20"
env_logger = "0.11.3" env_logger = "0.11.3"
serde_json = "1.0.107" serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "rt", "sync", "signal"] } tokio = {version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "process", "rt", "sync", "signal"] }
async-trait = "0.1.73"
futures-core = "0.3.28"
futures = "0.3.28" futures = "0.3.28"
clap = { version="4.4.11", features = ["derive"] } clap = { version="4.4.11", features = ["derive"] }
gtk = { package = "gtk4", version = "0.8.1", features = ["v4_2"], optional = true } gtk = { package = "gtk4", version = "0.8.1", features = ["v4_2"], optional = true }
adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional = true } adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional = true }
async-channel = { version = "2.1.1", optional = true } async-channel = { version = "2.1.1", optional = true }
keycode = "0.4.0"
once_cell = "1.19.0"
num_enum = "0.7.2"
hostname = "0.4.0" hostname = "0.4.0"
slab = "0.4.9" slab = "0.4.9"
endi = "1.1.0"
thiserror = "1.0.61"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.148" libc = "0.2.148"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version="0.31.1", optional = true }
wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true }
reis = { version = "0.2", features = [ "tokio" ], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.54.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[build-dependencies] [build-dependencies]
glib-build-tools = { version = "0.19.0", optional = true } glib-build-tools = { version = "0.19.0", optional = true }
[features] [features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"] default = [ "wayland", "x11", "xdg_desktop_portal", "libei", "gtk" ]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc" ] wayland = [ "input-capture/wayland", "input-emulation/wayland" ]
x11 = ["dep:x11"] x11 = [ "input-capture/x11", "input-emulation/x11" ]
xdg_desktop_portal = ["dep:ashpd"] xdg_desktop_portal = [ "input-emulation/xdg_desktop_portal" ]
libei = ["dep:reis", "dep:ashpd"] libei = [ "input-capture/libei", "input-emulation/libei" ]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"] gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"]

View File

@@ -69,7 +69,15 @@ Precompiled release binaries for Windows, MacOS and Linux are available in the [
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies). For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux ### Arch Linux
Lan Mouse is available on the AUR:
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
```sh
pacman -S lan-mouse
```
It is also available on the AUR:
```sh ```sh
# git version (includes latest changes) # git version (includes latest changes)
paru -S lan-mouse-git paru -S lan-mouse-git

View File

@@ -1,5 +1,7 @@
# example configuration # example configuration
# capture_backend = "LayerShell"
# release bind # release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ] release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]

12
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1710806803, "lastModified": 1716293225,
"narHash": "sha256-qrxvLS888pNJFwJdK+hf1wpRCSQcqA6W5+Ox202NDa0=", "narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b06025f1533a1e07b6db3e75151caa155d1c7eb3", "rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -48,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1710987136, "lastModified": 1716257780,
"narHash": "sha256-Q8GRdlAIKZ8tJUXrbcRO1pA33AdoPfTUirsSnmGQnOU=", "narHash": "sha256-R+NjvJzKEkTVCmdrKRfPE4liX/KMGVqGUwwS5H8ET8A=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "97596b54ac34ad8184ca1eef44b1ec2e5c2b5f9e", "rev": "4e5e3d2c5c9b2721bd266f9e43c14e96811b89d2",
"type": "github" "type": "github"
}, },
"original": { "original": {

48
input-capture/Cargo.toml Normal file
View File

@@ -0,0 +1,48 @@
[package]
name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
[dependencies]
anyhow = "1.0.86"
futures = "0.3.28"
futures-core = "0.3.30"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.1.0" }
memmap = "0.7"
tempfile = "3.8"
thiserror = "1.0.61"
tokio = { version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "process", "rt", "sync", "signal"] }
once_cell = "1.19.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version="0.31.1", optional = true }
wayland-protocols = { version="0.32.1", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.3.1", features=["client"], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true }
reis = { version = "0.2", features = [ "tokio" ], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.57.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[features]
default = ["wayland", "x11", "libei"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr" ]
x11 = ["dep:x11"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -4,10 +4,9 @@ use std::task::{Context, Poll};
use futures_core::Stream; use futures_core::Stream;
use crate::capture::InputCapture; use input_event::Event;
use crate::event::Event;
use crate::client::{ClientEvent, ClientHandle}; use super::{CaptureHandle, InputCapture, Position};
pub struct DummyInputCapture {} pub struct DummyInputCapture {}
@@ -24,7 +23,11 @@ impl Default for DummyInputCapture {
} }
impl InputCapture for DummyInputCapture { impl InputCapture for DummyInputCapture {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> { fn create(&mut self, _handle: CaptureHandle, _pos: Position) -> io::Result<()> {
Ok(())
}
fn destroy(&mut self, _handle: CaptureHandle) -> io::Result<()> {
Ok(()) Ok(())
} }
@@ -34,7 +37,7 @@ impl InputCapture for DummyInputCapture {
} }
impl Stream for DummyInputCapture { impl Stream for DummyInputCapture {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending Poll::Pending

120
input-capture/src/error.rs Normal file
View File

@@ -0,0 +1,120 @@
use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use std::io;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::{EiConvertEventStreamError, HandshakeError};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("error in libei stream: {inner:?}")]
pub struct ReisConvertEventStreamError {
inner: EiConvertEventStreamError,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl From<EiConvertEventStreamError> for ReisConvertEventStreamError {
fn from(e: EiConvertEventStreamError) -> Self {
Self { inner: e }
}
}
#[derive(Debug, Error)]
pub enum CaptureError {
#[error("activation stream closed unexpectedly")]
ActivationClosed,
#[error("libei stream was closed")]
EndOfStream,
#[error("io error: `{0}`")]
Io(#[from] std::io::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error in libei stream: `{0}`")]
Reis(#[from] ReisConvertEventStreamError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei handshake failed: `{0}`")]
Handshake(#[from] HandshakeError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error(transparent)]
Portal(#[from] ashpd::Error),
}
#[derive(Debug, Error)]
pub enum CaptureCreationError {
#[error("no backend available")]
NoAvailableBackend,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error creating input-capture-portal backend: `{0}`")]
Libei(#[from] LibeiCaptureCreationError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[error("error creating layer-shell capture backend: `{0}`")]
LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[error("error creating x11 capture backend: `{0}`")]
X11(#[from] X11InputCaptureCreationError),
#[cfg(target_os = "macos")]
#[error("error creating macos capture backend: `{0}`")]
Macos(#[from] MacOSInputCaptureCreationError),
#[cfg(windows)]
#[error("error creating windows capture backend")]
Windows,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiCaptureCreationError {
#[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("{protocol} protocol not supported: {inner}")]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError {
#[error(transparent)]
Connect(#[from] ConnectError),
#[error(transparent)]
Global(#[from] GlobalError),
#[error(transparent)]
Wayland(#[from] WaylandError),
#[error(transparent)]
Bind(#[from] WaylandBindError),
#[error(transparent)]
Dispatch(#[from] DispatchError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11InputCaptureCreationError {
#[error("X11 input capture is not yet implemented :(")]
NotImplemented,
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacOSInputCaptureCreationError {
#[error("MacOS input capture is not yet implemented :(")]
NotImplemented,
}

159
input-capture/src/lib.rs Normal file
View File

@@ -0,0 +1,159 @@
use std::{fmt::Display, io};
use futures_core::Stream;
use input_event::Event;
pub use error::{CaptureCreationError, CaptureError};
pub mod error;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
/// fallback input capture (does not produce events)
pub mod dummy;
pub type CaptureHandle = u64;
#[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 trait InputCapture: Stream<Item = io::Result<(CaptureHandle, Event)>> + Unpin {
/// create a new client with the given id
fn create(&mut self, id: CaptureHandle, pos: Position) -> io::Result<()>;
/// destroy the client with the given id, if it exists
fn destroy(&mut self, id: CaptureHandle) -> io::Result<()>;
/// release mouse
fn release(&mut self) -> io::Result<()>;
}
pub async fn create_backend(
backend: Backend,
) -> Result<Box<dyn InputCapture<Item = io::Result<(CaptureHandle, Event)>>>, 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()?)),
Backend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())),
}
}
pub async fn create(
backend: Option<Backend>,
) -> Result<Box<dyn InputCapture<Item = io::Result<(CaptureHandle, Event)>>>, 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,
Backend::Dummy,
] {
match create_backend(backend).await {
Ok(b) => {
log::info!("using capture backend: {backend}");
return Ok(b);
}
Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"),
}
}
Err(CaptureCreationError::NoAvailableBackend)
}

View File

@@ -1,4 +1,3 @@
use anyhow::{anyhow, Result};
use ashpd::{ use ashpd::{
desktop::{ desktop::{
input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones}, input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones},
@@ -30,23 +29,26 @@ use tokio::{
use futures_core::Stream; use futures_core::Stream;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::{ use input_event::{Event, KeyboardEvent, PointerEvent};
capture::InputCapture as LanMouseInputCapture,
client::{ClientEvent, ClientHandle, Position}, use crate::error::{CaptureError, ReisConvertEventStreamError};
event::{Event, KeyboardEvent, PointerEvent},
use super::{
error::LibeiCaptureCreationError, CaptureHandle, InputCapture as LanMouseInputCapture, Position,
}; };
#[derive(Debug)] #[derive(Debug)]
enum ProducerEvent { enum ProducerEvent {
Release, Release,
ClientEvent(ClientEvent), Create(CaptureHandle, Position),
Destroy(CaptureHandle),
} }
#[allow(dead_code)] #[allow(dead_code)]
pub struct LibeiInputCapture<'a> { pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>, input_capture: Pin<Box<InputCapture<'a>>>,
libei_task: JoinHandle<Result<()>>, libei_task: JoinHandle<Result<(), CaptureError>>,
event_rx: tokio::sync::mpsc::Receiver<(ClientHandle, Event)>, event_rx: tokio::sync::mpsc::Receiver<(CaptureHandle, Event)>,
notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>, notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>,
} }
@@ -79,9 +81,9 @@ fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
fn select_barriers( fn select_barriers(
zones: &Zones, zones: &Zones,
clients: &Vec<(ClientHandle, Position)>, clients: &Vec<(CaptureHandle, Position)>,
next_barrier_id: &mut u32, next_barrier_id: &mut u32,
) -> (Vec<Barrier>, HashMap<BarrierID, ClientHandle>) { ) -> (Vec<Barrier>, HashMap<BarrierID, CaptureHandle>) {
let mut client_for_barrier = HashMap::new(); let mut client_for_barrier = HashMap::new();
let mut barriers: Vec<Barrier> = vec![]; let mut barriers: Vec<Barrier> = vec![];
@@ -105,9 +107,9 @@ fn select_barriers(
async fn update_barriers( async fn update_barriers(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_>, session: &Session<'_>,
active_clients: &Vec<(ClientHandle, Position)>, active_clients: &Vec<(CaptureHandle, Position)>,
next_barrier_id: &mut u32, next_barrier_id: &mut u32,
) -> Result<HashMap<BarrierID, ClientHandle>> { ) -> Result<HashMap<BarrierID, CaptureHandle>, ashpd::Error> {
let zones = input_capture.zones(session).await?.response()?; let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}"); log::debug!("zones: {zones:?}");
@@ -131,7 +133,7 @@ impl<'a> Drop for LibeiInputCapture<'a> {
async fn create_session<'a>( async fn create_session<'a>(
input_capture: &'a InputCapture<'a>, input_capture: &'a InputCapture<'a>,
) -> Result<(Session<'a>, BitFlags<Capabilities>)> { ) -> std::result::Result<(Session<'a>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session"); log::debug!("creating input capture session");
let (session, capabilities) = loop { let (session, capabilities) = loop {
match input_capture match input_capture
@@ -153,7 +155,7 @@ async fn create_session<'a>(
async fn connect_to_eis( async fn connect_to_eis(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_>, session: &Session<'_>,
) -> Result<(ei::Context, EiConvertEventStream)> { ) -> Result<(ei::Context, EiConvertEventStream), CaptureError> {
log::debug!("connect_to_eis"); log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?; let fd = input_capture.connect_to_eis(session).await?;
@@ -164,17 +166,13 @@ async fn connect_to_eis(
// create ei context // create ei context
let context = ei::Context::new(stream)?; let context = ei::Context::new(stream)?;
let mut event_stream = EiEventStream::new(context.clone())?; let mut event_stream = EiEventStream::new(context.clone())?;
let response = match reis::tokio::ei_handshake( let response = reis::tokio::ei_handshake(
&mut event_stream, &mut event_stream,
"de.feschber.LanMouse", "de.feschber.LanMouse",
ei::handshake::ContextType::Receiver, ei::handshake::ContextType::Receiver,
&INTERFACES, &INTERFACES,
) )
.await .await?;
{
Ok(res) => res,
Err(e) => return Err(anyhow!("ei handshake failed: {e:?}")),
};
let event_stream = EiConvertEventStream::new(event_stream, response.serial); let event_stream = EiConvertEventStream::new(event_stream, response.serial);
Ok((context, event_stream)) Ok((context, event_stream))
@@ -183,15 +181,15 @@ async fn connect_to_eis(
async fn libei_event_handler( async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream, mut ei_event_stream: EiConvertEventStream,
context: ei::Context, context: ei::Context,
event_tx: Sender<(ClientHandle, Event)>, event_tx: Sender<(CaptureHandle, Event)>,
current_client: Rc<Cell<Option<ClientHandle>>>, current_client: Rc<Cell<Option<CaptureHandle>>>,
) -> Result<()> { ) -> Result<(), CaptureError> {
loop { loop {
let ei_event = match ei_event_stream.next().await { let ei_event = ei_event_stream
Some(Ok(event)) => event, .next()
Some(Err(e)) => return Err(anyhow!("libei connection closed: {e:?}")), .await
None => return Err(anyhow!("libei connection closed")), .ok_or(CaptureError::EndOfStream)?
}; .map_err(ReisConvertEventStreamError::from)?;
log::trace!("from ei: {ei_event:?}"); log::trace!("from ei: {ei_event:?}");
let client = current_client.get(); let client = current_client.get();
handle_ei_event(ei_event, client, &context, &event_tx).await; handle_ei_event(ei_event, client, &context, &event_tx).await;
@@ -200,137 +198,27 @@ async fn libei_event_handler(
async fn wait_for_active_client( async fn wait_for_active_client(
notify_rx: &mut Receiver<ProducerEvent>, notify_rx: &mut Receiver<ProducerEvent>,
active_clients: &mut Vec<(ClientHandle, Position)>, active_clients: &mut Vec<(CaptureHandle, Position)>,
) -> Result<()> { ) {
// wait for a client update // wait for a client update
while let Some(producer_event) = notify_rx.recv().await { while let Some(producer_event) = notify_rx.recv().await {
if let ProducerEvent::ClientEvent(c) = producer_event { if let ProducerEvent::Create(c, p) = producer_event {
handle_producer_event(ProducerEvent::ClientEvent(c), active_clients)?; handle_producer_event(ProducerEvent::Create(c, p), active_clients);
break; break;
} }
} }
Ok(())
} }
impl<'a> LibeiInputCapture<'a> { impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> Result<Self> { pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?); let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>; let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
let mut first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
let (event_tx, event_rx) = tokio::sync::mpsc::channel(32); let (event_tx, event_rx) = tokio::sync::mpsc::channel(32);
let (notify_tx, mut notify_rx) = tokio::sync::mpsc::channel(32); let (notify_tx, notify_rx) = tokio::sync::mpsc::channel(32);
let libei_task = tokio::task::spawn_local(async move { let capture = do_capture(input_capture_ptr, notify_rx, first_session, event_tx);
/* safety: libei_task does not outlive Self */ let libei_task = tokio::task::spawn_local(capture);
let input_capture = unsafe { &*input_capture_ptr };
let mut active_clients: Vec<(ClientHandle, Position)> = vec![];
let mut next_barrier_id = 1u32;
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to recreated when the barriers are updated */
loop {
// otherwise it asks to capture input even with no active clients
if active_clients.is_empty() {
wait_for_active_client(&mut notify_rx, &mut active_clients).await?;
continue;
}
let current_client = Rc::new(Cell::new(None));
// create session
let (session, _) = match first_session.take() {
Some(s) => s,
_ => create_session(input_capture).await?,
};
// connect to eis server
let (context, ei_event_stream) = connect_to_eis(input_capture, &session).await?;
// async event task
let mut ei_task: JoinHandle<Result<(), anyhow::Error>> =
tokio::task::spawn_local(libei_event_handler(
ei_event_stream,
context,
event_tx.clone(),
current_client.clone(),
));
let mut activated = input_capture.receive_activated().await?;
let mut zones_changed = input_capture.receive_zones_changed().await?;
// set barriers
let client_for_barrier_id = update_barriers(
input_capture,
&session,
&active_clients,
&mut next_barrier_id,
)
.await?;
log::debug!("enabling session");
input_capture.enable(&session).await?;
loop {
tokio::select! {
activated = activated.next() => {
let activated = activated.ok_or(anyhow!("error receiving activation token"))?;
log::debug!("activated: {activated:?}");
let client = *client_for_barrier_id
.get(&activated.barrier_id())
.expect("invalid barrier id");
current_client.replace(Some(client));
event_tx.send((client, Event::Enter())).await?;
tokio::select! {
producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed");
if handle_producer_event(producer_event, &mut active_clients)? {
break; /* clients updated */
}
}
zones_changed = zones_changed.next() => {
log::debug!("zones changed: {zones_changed:?}");
break;
}
res = &mut ei_task => {
if let Err(e) = res.expect("ei task paniced") {
log::warn!("libei task exited: {e}");
}
break;
}
}
release_capture(
input_capture,
&session,
activated,
client,
&active_clients,
).await?;
}
producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed");
if handle_producer_event(producer_event, &mut active_clients)? {
/* clients updated */
break;
}
},
res = &mut ei_task => {
if let Err(e) = res.expect("ei task paniced") {
log::warn!("libei task exited: {e}");
}
break;
}
}
}
ei_task.abort();
input_capture.disable(&session).await?;
}
});
let producer = Self { let producer = Self {
input_capture, input_capture,
@@ -343,13 +231,139 @@ impl<'a> LibeiInputCapture<'a> {
} }
} }
async fn do_capture<'a>(
input_capture_ptr: *const InputCapture<'static>,
mut notify_rx: Receiver<ProducerEvent>,
mut first_session: Option<(Session<'a>, BitFlags<Capabilities>)>,
event_tx: Sender<(CaptureHandle, Event)>,
) -> Result<(), CaptureError> {
/* safety: libei_task does not outlive Self */
let input_capture = unsafe { &*input_capture_ptr };
let mut active_clients: Vec<(CaptureHandle, Position)> = vec![];
let mut next_barrier_id = 1u32;
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to recreated when the barriers are updated */
loop {
// otherwise it asks to capture input even with no active clients
if active_clients.is_empty() {
wait_for_active_client(&mut notify_rx, &mut active_clients).await;
if notify_rx.is_closed() {
break Ok(());
} else {
continue;
}
}
let current_client = Rc::new(Cell::new(None));
// create session
let (session, _) = match first_session.take() {
Some(s) => s,
_ => create_session(input_capture).await?,
};
// connect to eis server
let (context, ei_event_stream) = connect_to_eis(input_capture, &session).await?;
// async event task
let mut ei_task: JoinHandle<Result<(), CaptureError>> =
tokio::task::spawn_local(libei_event_handler(
ei_event_stream,
context,
event_tx.clone(),
current_client.clone(),
));
let mut activated = input_capture.receive_activated().await?;
let mut zones_changed = input_capture.receive_zones_changed().await?;
// set barriers
let client_for_barrier_id = update_barriers(
input_capture,
&session,
&active_clients,
&mut next_barrier_id,
)
.await?;
log::debug!("enabling session");
input_capture.enable(&session).await?;
loop {
tokio::select! {
activated = activated.next() => {
let activated = activated.ok_or(CaptureError::ActivationClosed)?;
log::debug!("activated: {activated:?}");
let client = *client_for_barrier_id
.get(&activated.barrier_id())
.expect("invalid barrier id");
current_client.replace(Some(client));
if event_tx.send((client, Event::Enter())).await.is_err() {
break;
};
tokio::select! {
producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed");
if handle_producer_event(producer_event, &mut active_clients) {
break; /* clients updated */
}
}
zones_changed = zones_changed.next() => {
log::debug!("zones changed: {zones_changed:?}");
break;
}
res = &mut ei_task => {
if let Err(e) = res.expect("ei task paniced") {
log::warn!("libei task exited: {e}");
}
break;
}
}
release_capture(
input_capture,
&session,
activated,
client,
&active_clients,
).await?;
}
producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed");
if handle_producer_event(producer_event, &mut active_clients) {
/* clients updated */
break;
}
},
res = &mut ei_task => {
if let Err(e) = res.expect("ei task paniced") {
log::warn!("libei task exited: {e}");
}
break;
}
}
}
ei_task.abort();
input_capture.disable(&session).await?;
if event_tx.is_closed() {
break Ok(());
}
}
}
async fn release_capture( async fn release_capture(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_>, session: &Session<'_>,
activated: Activated, activated: Activated,
current_client: ClientHandle, current_client: CaptureHandle,
active_clients: &[(ClientHandle, Position)], active_clients: &[(CaptureHandle, Position)],
) -> Result<()> { ) -> Result<(), CaptureError> {
log::debug!("releasing input capture {}", activated.activation_id()); log::debug!("releasing input capture {}", activated.activation_id());
let (x, y) = activated.cursor_position(); let (x, y) = activated.cursor_position();
let pos = active_clients let pos = active_clients
@@ -375,28 +389,27 @@ async fn release_capture(
fn handle_producer_event( fn handle_producer_event(
producer_event: ProducerEvent, producer_event: ProducerEvent,
active_clients: &mut Vec<(ClientHandle, Position)>, active_clients: &mut Vec<(CaptureHandle, Position)>,
) -> Result<bool> { ) -> bool {
log::debug!("handling event: {producer_event:?}"); log::debug!("handling event: {producer_event:?}");
let updated = match producer_event { match producer_event {
ProducerEvent::Release => false, ProducerEvent::Release => false,
ProducerEvent::ClientEvent(ClientEvent::Create(c, p)) => { ProducerEvent::Create(c, p) => {
active_clients.push((c, p)); active_clients.push((c, p));
true true
} }
ProducerEvent::ClientEvent(ClientEvent::Destroy(c)) => { ProducerEvent::Destroy(c) => {
active_clients.retain(|(h, _)| *h != c); active_clients.retain(|(h, _)| *h != c);
true true
} }
}; }
Ok(updated)
} }
async fn handle_ei_event( async fn handle_ei_event(
ei_event: EiEvent, ei_event: EiEvent,
current_client: Option<ClientHandle>, current_client: Option<CaptureHandle>,
context: &ei::Context, context: &ei::Context,
event_tx: &Sender<(ClientHandle, Event)>, event_tx: &Sender<(CaptureHandle, Event)>,
) { ) {
match ei_event { match ei_event {
EiEvent::SeatAdded(s) => { EiEvent::SeatAdded(s) => {
@@ -439,8 +452,8 @@ async fn handle_ei_event(
EiEvent::PointerMotion(motion) => { EiEvent::PointerMotion(motion) => {
let motion_event = PointerEvent::Motion { let motion_event = PointerEvent::Motion {
time: motion.time as u32, time: motion.time as u32,
relative_x: motion.dx as f64, dx: motion.dx as f64,
relative_y: motion.dy as f64, dy: motion.dy as f64,
}; };
if let Some(current_client) = current_client { if let Some(current_client) = current_client {
event_tx event_tx
@@ -545,12 +558,18 @@ async fn handle_ei_event(
} }
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> { impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
fn notify(&mut self, event: ClientEvent) -> io::Result<()> { fn create(&mut self, handle: super::CaptureHandle, pos: super::Position) -> io::Result<()> {
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
log::debug!("notifying {event:?}"); let _ = notify_tx.send(ProducerEvent::Create(handle, pos)).await;
let _ = notify_tx.send(ProducerEvent::ClientEvent(event)).await; });
log::debug!("done !"); Ok(())
}
fn destroy(&mut self, handle: super::CaptureHandle) -> io::Result<()> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
let _ = notify_tx.send(ProducerEvent::Destroy(handle)).await;
}); });
Ok(()) Ok(())
} }
@@ -558,7 +577,6 @@ impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
fn release(&mut self) -> io::Result<()> { fn release(&mut self) -> io::Result<()> {
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
log::debug!("notifying Release");
let _ = notify_tx.send(ProducerEvent::Release).await; let _ = notify_tx.send(ProducerEvent::Release).await;
}); });
Ok(()) Ok(())
@@ -566,7 +584,7 @@ impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
} }
impl<'a> Stream for LibeiInputCapture<'a> { impl<'a> Stream for LibeiInputCapture<'a> {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) { match ready!(self.event_rx.poll_recv(cx)) {

View File

@@ -0,0 +1,35 @@
use crate::{error::MacOSInputCaptureCreationError, CaptureHandle, InputCapture, Position};
use futures_core::Stream;
use input_event::Event;
use std::task::{Context, Poll};
use std::{io, pin::Pin};
pub struct MacOSInputCapture;
impl MacOSInputCapture {
pub fn new() -> std::result::Result<Self, MacOSInputCaptureCreationError> {
Err(MacOSInputCaptureCreationError::NotImplemented)
}
}
impl Stream for MacOSInputCapture {
type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
}
}
impl InputCapture for MacOSInputCapture {
fn create(&mut self, _id: CaptureHandle, _pos: Position) -> io::Result<()> {
Ok(())
}
fn destroy(&mut self, _id: CaptureHandle) -> io::Result<()> {
Ok(())
}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@@ -1,9 +1,3 @@
use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle, Position},
};
use anyhow::{anyhow, Result};
use futures_core::Stream; use futures_core::Stream;
use memmap::MmapOptions; use memmap::MmapOptions;
use std::{ use std::{
@@ -66,7 +60,12 @@ use wayland_client::{
use tempfile; use tempfile;
use crate::event::{Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent};
use super::{
error::{LayerShellCaptureCreationError, WaylandBindError},
CaptureHandle, InputCapture, Position,
};
struct Globals { struct Globals {
compositor: wl_compositor::WlCompositor, compositor: wl_compositor::WlCompositor,
@@ -103,14 +102,15 @@ struct State {
pointer_lock: Option<ZwpLockedPointerV1>, pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>, rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>, shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
client_for_window: Vec<(Rc<Window>, ClientHandle)>, client_for_window: Vec<(Rc<Window>, CaptureHandle)>,
focused: Option<(Rc<Window>, ClientHandle)>, focused: Option<(Rc<Window>, CaptureHandle)>,
g: Globals, g: Globals,
wayland_fd: OwnedFd, wayland_fd: OwnedFd,
read_guard: Option<ReadEventsGuard>, read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>, qh: QueueHandle<Self>,
pending_events: VecDeque<(ClientHandle, Event)>, pending_events: VecDeque<(CaptureHandle, Event)>,
output_info: Vec<(WlOutput, OutputInfo)>, output_info: Vec<(WlOutput, OutputInfo)>,
scroll_discrete_pending: bool,
} }
struct Inner { struct Inner {
@@ -257,64 +257,37 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
} }
impl WaylandInputCapture { impl WaylandInputCapture {
pub fn new() -> Result<Self> { pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
let conn = match Connection::connect_to_env() { let conn = Connection::connect_to_env()?;
Ok(c) => c, let (g, mut queue) = registry_queue_init::<State>(&conn)?;
Err(e) => return Err(anyhow!("could not connect to wayland compositor: {e:?}")),
};
let (g, mut queue) = match registry_queue_init::<State>(&conn) {
Ok(q) => q,
Err(e) => return Err(anyhow!("failed to initialize wl_registry: {e:?}")),
};
let qh = queue.handle(); let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) { let compositor: wl_compositor::WlCompositor = g
Ok(compositor) => compositor, .bind(&qh, 4..=5, ())
Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")), .map_err(|e| WaylandBindError::new(e, "wl_compositor 4..=5"))?;
}; let xdg_output_manager: ZxdgOutputManagerV1 = g
.bind(&qh, 1..=3, ())
let xdg_output_manager: ZxdgOutputManagerV1 = match g.bind(&qh, 1..=3, ()) { .map_err(|e| WaylandBindError::new(e, "xdg_output_manager 1..=3"))?;
Ok(xdg_output_manager) => xdg_output_manager, let shm: wl_shm::WlShm = g
Err(_) => return Err(anyhow!("xdg_output not supported!")), .bind(&qh, 1..=1, ())
}; .map_err(|e| WaylandBindError::new(e, "wl_shm"))?;
let layer_shell: ZwlrLayerShellV1 = g
let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) { .bind(&qh, 3..=4, ())
Ok(wl_shm) => wl_shm, .map_err(|e| WaylandBindError::new(e, "wlr_layer_shell 3..=4"))?;
Err(_) => return Err(anyhow!("wl_shm v1 not supported")), let seat: wl_seat::WlSeat = g
}; .bind(&qh, 7..=8, ())
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
let layer_shell: ZwlrLayerShellV1 = match g.bind(&qh, 3..=4, ()) {
Ok(layer_shell) => layer_shell,
Err(_) => return Err(anyhow!("zwlr_layer_shell_v1 >= v3 not supported - required to display a surface at the edge of the screen")),
};
let seat: wl_seat::WlSeat = match g.bind(&qh, 7..=8, ()) {
Ok(wl_seat) => wl_seat,
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
};
let pointer_constraints: ZwpPointerConstraintsV1 = match g.bind(&qh, 1..=1, ()) {
Ok(pointer_constraints) => pointer_constraints,
Err(_) => return Err(anyhow!("zwp_pointer_constraints_v1 not supported")),
};
let relative_pointer_manager: ZwpRelativePointerManagerV1 = match g.bind(&qh, 1..=1, ()) {
Ok(relative_pointer_manager) => relative_pointer_manager,
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
};
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 =
match g.bind(&qh, 1..=1, ()) {
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
Err(_) => {
return Err(anyhow!(
"zwp_keyboard_shortcuts_inhibit_manager_v1 not supported"
))
}
};
let pointer_constraints: ZwpPointerConstraintsV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_pointer_constraints_v1"))?;
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?;
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"))?;
let outputs = vec![]; let outputs = vec![];
let g = Globals { let g = Globals {
@@ -351,6 +324,7 @@ impl WaylandInputCapture {
read_guard: None, read_guard: None,
pending_events: VecDeque::new(), pending_events: VecDeque::new(),
output_info: vec![], output_info: vec![],
scroll_discrete_pending: false,
}; };
// dispatch registry to () again, in order to read all wl_outputs // dispatch registry to () again, in order to read all wl_outputs
@@ -393,11 +367,11 @@ impl WaylandInputCapture {
Ok(WaylandInputCapture(inner)) Ok(WaylandInputCapture(inner))
} }
fn add_client(&mut self, handle: ClientHandle, pos: Position) { fn add_client(&mut self, handle: CaptureHandle, pos: Position) {
self.0.get_mut().state.add_client(handle, pos); self.0.get_mut().state.add_client(handle, pos);
} }
fn delete_client(&mut self, handle: ClientHandle) { fn delete_client(&mut self, handle: CaptureHandle) {
let inner = self.0.get_mut(); let inner = self.0.get_mut();
// remove all windows corresponding to this client // remove all windows corresponding to this client
while let Some(i) = inner while let Some(i) = inner
@@ -495,7 +469,7 @@ impl State {
} }
} }
fn add_client(&mut self, client: ClientHandle, pos: Position) { fn add_client(&mut self, client: CaptureHandle, pos: Position) {
let outputs = get_output_configuration(self, pos); let outputs = get_output_configuration(self, pos);
log::debug!("outputs: {outputs:?}"); log::debug!("outputs: {outputs:?}");
@@ -588,15 +562,13 @@ impl Inner {
} }
impl InputCapture for WaylandInputCapture { impl InputCapture for WaylandInputCapture {
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> { fn create(&mut self, handle: CaptureHandle, pos: Position) -> io::Result<()> {
match client_event { self.add_client(handle, pos);
ClientEvent::Create(handle, pos) => { let inner = self.0.get_mut();
self.add_client(handle, pos); inner.flush_events()
} }
ClientEvent::Destroy(handle) => { fn destroy(&mut self, handle: CaptureHandle) -> io::Result<()> {
self.delete_client(handle); self.delete_client(handle);
}
}
let inner = self.0.get_mut(); let inner = self.0.get_mut();
inner.flush_events() inner.flush_events()
} }
@@ -610,7 +582,7 @@ impl InputCapture for WaylandInputCapture {
} }
impl Stream for WaylandInputCapture { impl Stream for WaylandInputCapture {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() { if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
@@ -752,17 +724,25 @@ impl Dispatch<WlPointer, ()> for State {
} }
wl_pointer::Event::Axis { time, axis, value } => { wl_pointer::Event::Axis { time, axis, value } => {
let (_, client) = app.focused.as_ref().unwrap(); let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push_back(( if app.scroll_discrete_pending {
*client, // each axisvalue120 event is coupled with
Event::Pointer(PointerEvent::Axis { // a corresponding axis event, which needs to
time, // be ignored to not duplicate the scrolling
axis: u32::from(axis) as u8, app.scroll_discrete_pending = false;
value, } else {
}), app.pending_events.push_back((
)); *client,
Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
}),
));
}
} }
wl_pointer::Event::AxisValue120 { axis, value120 } => { wl_pointer::Event::AxisValue120 { axis, value120 } => {
let (_, client) = app.focused.as_ref().unwrap(); let (_, client) = app.focused.as_ref().unwrap();
app.scroll_discrete_pending = true;
app.pending_events.push_back(( app.pending_events.push_back((
*client, *client,
Event::Pointer(PointerEvent::AxisDiscrete120 { Event::Pointer(PointerEvent::AxisDiscrete120 {
@@ -857,21 +837,16 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
if let zwp_relative_pointer_v1::Event::RelativeMotion { if let zwp_relative_pointer_v1::Event::RelativeMotion {
utime_hi, utime_hi,
utime_lo, utime_lo,
dx: _, dx_unaccel: dx,
dy: _, dy_unaccel: dy,
dx_unaccel: surface_x, ..
dy_unaccel: surface_y,
} = event } = event
{ {
if let Some((_window, client)) = &app.focused { if let Some((_window, client)) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32; let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push_back(( app.pending_events.push_back((
*client, *client,
Event::Pointer(PointerEvent::Motion { Event::Pointer(PointerEvent::Motion { time, dx, dy }),
time,
relative_x: surface_x,
relative_y: surface_y,
}),
)); ));
} }
} }

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use core::task::{Context, Poll}; use core::task::{Context, Poll};
use futures::Stream; use futures::Stream;
use once_cell::unsync::Lazy; use once_cell::unsync::Lazy;
@@ -9,7 +8,7 @@ use std::ptr::{addr_of, addr_of_mut};
use futures::executor::block_on; use futures::executor::block_on;
use std::default::Default; use std::default::Default;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc; use std::sync::{mpsc, Mutex};
use std::task::ready; use std::task::ready;
use std::{io, pin::Pin, thread}; use std::{io, pin::Pin, thread};
use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::mpsc::{channel, Receiver, Sender};
@@ -32,25 +31,25 @@ use windows::Win32::UI::WindowsAndMessaging::{
WNDPROC, WNDPROC,
}; };
use crate::client::Position; use input_event::{
use crate::event::{ scancode::{self, Linux},
KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use crate::scancode::Linux;
use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle},
event::Event,
scancode,
}; };
use super::{CaptureHandle, InputCapture, Position};
enum Request {
Create(CaptureHandle, Position),
Destroy(CaptureHandle),
}
pub struct WindowsInputCapture { pub struct WindowsInputCapture {
event_rx: Receiver<(ClientHandle, Event)>, event_rx: Receiver<(CaptureHandle, Event)>,
msg_thread: Option<std::thread::JoinHandle<()>>, msg_thread: Option<std::thread::JoinHandle<()>>,
} }
enum EventType { enum EventType {
ClientEvent = 0, Request = 0,
Release = 1, Release = 1,
Exit = 2, Exit = 2,
} }
@@ -59,15 +58,28 @@ unsafe fn signal_message_thread(event_type: EventType) {
if let Some(event_tid) = get_event_tid() { if let Some(event_tid) = get_event_tid() {
PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap(); PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap();
} else { } else {
log::warn!("lost event"); panic!();
} }
} }
impl InputCapture for WindowsInputCapture { impl InputCapture for WindowsInputCapture {
fn notify(&mut self, event: ClientEvent) -> io::Result<()> { fn create(&mut self, handle: CaptureHandle, pos: Position) -> io::Result<()> {
unsafe { unsafe {
EVENT_BUFFER.push(event); {
signal_message_thread(EventType::ClientEvent); let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Create(handle, pos));
}
signal_message_thread(EventType::Request);
}
Ok(())
}
fn destroy(&mut self, handle: CaptureHandle) -> io::Result<()> {
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Destroy(handle));
}
signal_message_thread(EventType::Request);
} }
Ok(()) Ok(())
} }
@@ -78,10 +90,10 @@ impl InputCapture for WindowsInputCapture {
} }
} }
static mut EVENT_BUFFER: Vec<ClientEvent> = Vec::new(); static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new());
static mut ACTIVE_CLIENT: Option<ClientHandle> = None; static mut ACTIVE_CLIENT: Option<CaptureHandle> = None;
static mut CLIENT_FOR_POS: Lazy<HashMap<Position, ClientHandle>> = Lazy::new(HashMap::new); static mut CLIENT_FOR_POS: Lazy<HashMap<Position, CaptureHandle>> = Lazy::new(HashMap::new);
static mut EVENT_TX: Option<Sender<(ClientHandle, Event)>> = None; static mut EVENT_TX: Option<Sender<(CaptureHandle, Event)>> = None;
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0); static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
unsafe fn set_event_tid(tid: u32) { unsafe fn set_event_tid(tid: u32) {
EVENT_THREAD_ID.store(tid, Ordering::SeqCst); EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
@@ -96,8 +108,7 @@ unsafe fn get_event_tid() -> Option<u32> {
static mut ENTRY_POINT: (i32, i32) = (0, 0); static mut ENTRY_POINT: (i32, i32) = (0, 0);
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> { fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
match wparam { match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button { WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0, time: 0,
@@ -133,15 +144,12 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y); let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT; let (ex, ey) = ENTRY_POINT;
let (dx, dy) = (x - ex, y - ey); let (dx, dy) = (x - ex, y - ey);
Some(PointerEvent::Motion { let (dx, dy) = (dx as f64, dy as f64);
time: 0, Some(PointerEvent::Motion { time: 0, dx, dy })
relative_x: dx as f64,
relative_y: dy as f64,
})
}, },
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 { WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0, axis: 0,
value: -(mouse_low_level.mouseData as i32), value: -(mouse_low_level.mouseData as i32 >> 16),
}), }),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => { WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16; let hb = mouse_low_level.mouseData >> 16;
@@ -167,8 +175,7 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
} }
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> { unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = let kybrdllhookstruct: KBDLLHOOKSTRUCT = *(lparam.0 as *const KBDLLHOOKSTRUCT);
*std::mem::transmute::<LPARAM, *const KBDLLHOOKSTRUCT>(lparam);
let mut scan_code = kybrdllhookstruct.scanCode; let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}"); log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) { if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
@@ -247,8 +254,7 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize { if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.is_some(); return ACTIVE_CLIENT.is_some();
} }
let mouse_low_level: MSLLHOOKSTRUCT = let mouse_low_level: MSLLHOOKSTRUCT = *(lparam.0 as *const MSLLHOOKSTRUCT);
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
static mut PREV_POS: Option<(i32, i32)> = None; static mut PREV_POS: Option<(i32, i32)> = None;
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y); let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.unwrap_or(curr_pos); let prev_pos = PREV_POS.unwrap_or(curr_pos);
@@ -359,7 +365,6 @@ fn enumerate_displays() -> Vec<RECT> {
break; break;
} }
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 { if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
log::info!("{:?}", device.DeviceName);
devices.push(device.DeviceName); devices.push(device.DeviceName);
} }
} }
@@ -538,28 +543,37 @@ fn message_thread(ready_tx: mpsc::Sender<()>) {
x if x == EventType::Release as usize => { x if x == EventType::Release as usize => {
ACTIVE_CLIENT.take(); ACTIVE_CLIENT.take();
} }
x if x == EventType::ClientEvent as usize => { x if x == EventType::Request as usize => {
while let Some(event) = EVENT_BUFFER.pop() { let requests = {
update_clients(event) let mut res = vec![];
let mut requests = REQUEST_BUFFER.lock().unwrap();
for request in requests.drain(..) {
res.push(request);
}
res
};
for request in requests {
update_clients(request)
} }
} }
_ => {} _ => {}
} }
} else { } else {
/* other messages for window_procs */ /* other messages for window_procs */
TranslateMessage(&msg); let _ = TranslateMessage(&msg);
DispatchMessageW(&msg); DispatchMessageW(&msg);
} }
} }
} }
} }
fn update_clients(client_event: ClientEvent) { fn update_clients(request: Request) {
match client_event { match request {
ClientEvent::Create(handle, pos) => { Request::Create(handle, pos) => {
unsafe { CLIENT_FOR_POS.insert(pos, handle) }; unsafe { CLIENT_FOR_POS.insert(pos, handle) };
} }
ClientEvent::Destroy(handle) => unsafe { Request::Destroy(handle) => unsafe {
for pos in [ for pos in [
Position::Left, Position::Left,
Position::Right, Position::Right,
@@ -578,7 +592,7 @@ fn update_clients(client_event: ClientEvent) {
} }
impl WindowsInputCapture { impl WindowsInputCapture {
pub(crate) fn new() -> Result<Self> { pub(crate) fn new() -> Self {
unsafe { unsafe {
let (tx, rx) = channel(10); let (tx, rx) = channel(10);
EVENT_TX.replace(tx); EVENT_TX.replace(tx);
@@ -586,16 +600,16 @@ impl WindowsInputCapture {
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx))); let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
/* wait for thread to set its id */ /* wait for thread to set its id */
ready_rx.recv().expect("channel closed"); ready_rx.recv().expect("channel closed");
Ok(Self { Self {
msg_thread, msg_thread,
event_rx: rx, event_rx: rx,
}) }
} }
} }
} }
impl Stream for WindowsInputCapture { impl Stream for WindowsInputCapture {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) { match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None), None => Poll::Ready(None),

43
input-capture/src/x11.rs Normal file
View File

@@ -0,0 +1,43 @@
use std::io;
use std::task::Poll;
use futures_core::Stream;
use super::InputCapture;
use input_event::Event;
use super::error::X11InputCaptureCreationError;
use super::{CaptureHandle, Position};
pub struct X11InputCapture {}
impl X11InputCapture {
pub fn new() -> std::result::Result<Self, X11InputCaptureCreationError> {
Err(X11InputCaptureCreationError::NotImplemented)
}
}
impl InputCapture for X11InputCapture {
fn create(&mut self, _id: CaptureHandle, _pos: Position) -> io::Result<()> {
Ok(())
}
fn destroy(&mut self, _id: CaptureHandle) -> io::Result<()> {
Ok(())
}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Stream for X11InputCapture {
type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

@@ -0,0 +1,47 @@
[package]
name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
[dependencies]
async-trait = "0.1.80"
futures = "0.3.28"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.1.0" }
thiserror = "1.0.61"
tokio = { version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "process", "rt", "sync", "signal"] }
once_cell = "1.19.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version="0.31.1", optional = true }
wayland-protocols = { version="0.32.1", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.3.1", features=["client"], optional = true }
wayland-protocols-misc = { version="0.3.1", features=["client"], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true }
reis = { version = "0.2", features = [ "tokio" ], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
keycode = "0.4.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.57.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc" ]
x11 = ["dep:x11"]
xdg_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -0,0 +1,29 @@
use async_trait::async_trait;
use input_event::Event;
use crate::error::EmulationError;
use super::{EmulationHandle, InputEmulation};
#[derive(Default)]
pub struct DummyEmulation;
impl DummyEmulation {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl InputEmulation for DummyEmulation {
async fn consume(
&mut self,
event: Event,
client_handle: EmulationHandle,
) -> Result<(), EmulationError> {
log::info!("received event: ({client_handle}) {event}");
Ok(())
}
async fn create(&mut self, _: EmulationHandle) {}
async fn destroy(&mut self, _: EmulationHandle) {}
}

View File

@@ -0,0 +1,144 @@
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::EiConvertEventStreamError;
use std::io;
use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::HandshakeError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("error in libei stream: {inner:?}")]
pub struct ReisConvertStreamError {
inner: EiConvertEventStreamError,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl From<EiConvertEventStreamError> for ReisConvertStreamError {
fn from(e: EiConvertEventStreamError) -> Self {
Self { inner: e }
}
}
#[derive(Debug, Error)]
pub enum EmulationError {
#[error("event stream closed")]
EndOfStream,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error flushing events: `{0}`")]
Libei(#[from] reis::event::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("")]
LibeiConvertStream(#[from] ReisConvertStreamError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(all(
unix,
any(feature = "xdg_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
#[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error),
#[error("io error: `{0}`")]
Io(#[from] io::Error),
}
#[derive(Debug, Error)]
pub enum EmulationCreationError {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[error("wlroots backend: `{0}`")]
Wlroots(#[from] WlrootsEmulationCreationError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei backend: `{0}`")]
Libei(#[from] LibeiEmulationCreationError),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[error("xdg-desktop-portal: `{0}`")]
Xdp(#[from] XdpEmulationCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[error("x11: `{0}`")]
X11(#[from] X11EmulationCreationError),
#[cfg(target_os = "macos")]
#[error("macos: `{0}`")]
MacOs(#[from] MacOSEmulationCreationError),
#[cfg(windows)]
#[error("windows: `{0}`")]
Windows(#[from] WindowsEmulationCreationError),
#[error("capture error")]
NoAvailableBackend,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum WlrootsEmulationCreationError {
#[error(transparent)]
Connect(#[from] ConnectError),
#[error(transparent)]
Global(#[from] GlobalError),
#[error(transparent)]
Wayland(#[from] WaylandError),
#[error(transparent)]
Bind(#[from] WaylandBindError),
#[error(transparent)]
Dispatch(#[from] DispatchError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("wayland protocol \"{protocol}\" not supported: {inner}")]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiEmulationCreationError {
#[error(transparent)]
Ashpd(#[from] ashpd::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Handshake(#[from] HandshakeError),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum XdpEmulationCreationError {
#[error(transparent)]
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11EmulationCreationError {
#[error("could not open display")]
OpenDisplay,
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacOSEmulationCreationError {
#[error("could not create event source")]
EventSourceCreation,
}
#[cfg(windows)]
#[derive(Debug, Error)]
pub enum WindowsEmulationCreationError {}

139
input-emulation/src/lib.rs Normal file
View File

@@ -0,0 +1,139 @@
use async_trait::async_trait;
use error::EmulationError;
use std::fmt::Display;
use input_event::Event;
use self::error::EmulationCreationError;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
pub mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
/// fallback input emulation (logs events)
pub mod dummy;
pub mod error;
pub type EmulationHandle = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
Xdp,
#[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 = "wayland", not(target_os = "macos")))]
Backend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => write!(f, "xdg-desktop-portal"),
#[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"),
}
}
}
#[async_trait]
pub trait InputEmulation: Send {
async fn consume(
&mut self,
event: Event,
handle: EmulationHandle,
) -> Result<(), EmulationError>;
async fn create(&mut self, handle: EmulationHandle);
async fn destroy(&mut self, handle: EmulationHandle);
}
pub async fn create_backend(
backend: Backend,
) -> Result<Box<dyn InputEmulation>, EmulationCreationError> {
match backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Backend::Wlroots => Ok(Box::new(wlroots::WlrootsEmulation::new()?)),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => Ok(Box::new(libei::LibeiEmulation::new().await?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11Emulation::new()?)),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => Ok(Box::new(
xdg_desktop_portal::DesktopPortalEmulation::new().await?,
)),
#[cfg(windows)]
Backend::Windows => Ok(Box::new(windows::WindowsEmulation::new()?)),
#[cfg(target_os = "macos")]
Backend::MacOs => Ok(Box::new(macos::MacOSEmulation::new()?)),
Backend::Dummy => Ok(Box::new(dummy::DummyEmulation::new())),
}
}
pub async fn create(
backend: Option<Backend>,
) -> Result<Box<dyn InputEmulation>, EmulationCreationError> {
if let Some(backend) = backend {
let b = create_backend(backend).await;
if b.is_ok() {
log::info!("using emulation backend: {backend}");
}
return b;
}
for backend in [
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Backend::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
Backend::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,
#[cfg(windows)]
Backend::Windows,
#[cfg(target_os = "macos")]
Backend::MacOs,
Backend::Dummy,
] {
match create_backend(backend).await {
Ok(b) => {
log::info!("using emulation backend: {backend}");
return Ok(b);
}
Err(e) => log::warn!("{e}"),
}
}
Err(EmulationCreationError::NoAvailableBackend)
}

View File

@@ -0,0 +1,331 @@
use futures::StreamExt;
use once_cell::sync::Lazy;
use std::{
collections::HashMap,
io,
os::{fd::OwnedFd, unix::net::UnixStream},
sync::{
atomic::{AtomicU32, Ordering},
Arc, RwLock,
},
time::{SystemTime, UNIX_EPOCH},
};
use tokio::task::JoinHandle;
use ashpd::{
desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
ResponseError,
},
WindowIdentifier,
};
use async_trait::async_trait;
use reis::{
ei::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
Pointer, Scroll,
},
event::{DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::{ei_handshake, EiConvertEventStream, EiEventStream},
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::{EmulationError, ReisConvertStreamError};
use super::{error::LibeiEmulationCreationError, EmulationHandle, InputEmulation};
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("ei_connection", 1);
m.insert("ei_callback", 1);
m.insert("ei_pingpong", 1);
m.insert("ei_seat", 1);
m.insert("ei_device", 2);
m.insert("ei_pointer", 1);
m.insert("ei_pointer_absolute", 1);
m.insert("ei_scroll", 1);
m.insert("ei_button", 1);
m.insert("ei_keyboard", 1);
m.insert("ei_touchscreen", 1);
m
});
#[derive(Clone, Default)]
struct Devices {
pointer: Arc<RwLock<Option<(ei::Device, ei::Pointer)>>>,
scroll: Arc<RwLock<Option<(ei::Device, ei::Scroll)>>>,
button: Arc<RwLock<Option<(ei::Device, ei::Button)>>>,
keyboard: Arc<RwLock<Option<(ei::Device, ei::Keyboard)>>>,
}
pub struct LibeiEmulation {
context: ei::Context,
devices: Devices,
serial: AtomicU32,
ei_task: JoinHandle<Result<(), EmulationError>>,
}
async fn get_ei_fd() -> Result<OwnedFd, ashpd::Error> {
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
let (session, _) = loop {
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
log::info!("requesting permission for input emulation");
match proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()
{
Ok(d) => break (session, d),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
log::warn!("request cancelled!");
continue;
}
e => e?,
};
};
proxy.connect_to_eis(&session).await
}
impl LibeiEmulation {
pub async fn new() -> Result<Self, LibeiEmulationCreationError> {
let eifd = get_ei_fd().await?;
let stream = UnixStream::from(eifd);
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
let mut events = EiEventStream::new(context.clone())?;
let handshake = ei_handshake(
&mut events,
"de.feschber.LanMouse",
ContextType::Sender,
&INTERFACES,
)
.await?;
let events = EiConvertEventStream::new(events, handshake.serial);
let devices = Devices::default();
let ei_handler = ei_event_handler(events, context.clone(), devices.clone());
let ei_task = tokio::task::spawn_local(ei_handler);
let serial = AtomicU32::new(handshake.serial);
Ok(Self {
serial,
context,
ei_task,
devices,
})
}
}
impl Drop for LibeiEmulation {
fn drop(&mut self) {
self.ei_task.abort();
}
}
#[async_trait]
impl InputEmulation for LibeiEmulation {
async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
match event {
Event::Pointer(p) => match p {
PointerEvent::Motion { time: _, dx, dy } => {
let pointer_device = self.devices.pointer.read().unwrap();
if let Some((d, p)) = pointer_device.as_ref() {
p.motion_relative(dx as f32, dy as f32);
d.frame(self.serial.load(Ordering::SeqCst), now);
}
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let button_device = self.devices.button.read().unwrap();
if let Some((d, b)) = button_device.as_ref() {
b.button(
button,
match state {
0 => ButtonState::Released,
_ => ButtonState::Press,
},
);
d.frame(self.serial.load(Ordering::SeqCst), now);
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let scroll_device = self.devices.scroll.read().unwrap();
if let Some((d, s)) = scroll_device.as_ref() {
match axis {
0 => s.scroll(0., value as f32),
_ => s.scroll(value as f32, 0.),
}
d.frame(self.serial.load(Ordering::SeqCst), now);
}
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let scroll_device = self.devices.scroll.read().unwrap();
if let Some((d, s)) = scroll_device.as_ref() {
match axis {
0 => s.scroll_discrete(0, value),
_ => s.scroll_discrete(value, 0),
}
d.frame(self.serial.load(Ordering::SeqCst), now);
}
}
PointerEvent::Frame {} => {}
},
Event::Keyboard(k) => match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let keyboard_device = self.devices.keyboard.read().unwrap();
if let Some((d, k)) = keyboard_device.as_ref() {
k.key(
key,
match state {
0 => KeyState::Released,
_ => KeyState::Press,
},
);
d.frame(self.serial.load(Ordering::SeqCst), now);
}
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
self.context
.flush()
.map_err(|e| io::Error::new(e.kind(), e))?;
Ok(())
}
async fn create(&mut self, _: EmulationHandle) {}
async fn destroy(&mut self, _: EmulationHandle) {}
}
async fn ei_event_handler(
mut events: EiConvertEventStream,
context: ei::Context,
devices: Devices,
) -> Result<(), EmulationError> {
loop {
let event = events
.next()
.await
.ok_or(EmulationError::EndOfStream)?
.map_err(ReisConvertStreamError::from)?;
const CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
log::debug!("{event:?}");
match event {
EiEvent::Disconnected(e) => {
log::debug!("ei disconnected: {e:?}");
break;
}
EiEvent::SeatAdded(e) => {
e.seat().bind_capabilities(CAPABILITIES);
}
EiEvent::SeatRemoved(e) => {
log::debug!("seat removed: {:?}", e.seat());
}
EiEvent::DeviceAdded(e) => {
let device_type = e.device().device_type();
log::debug!("device added: {device_type:?}");
e.device().device();
let device = e.device();
if let Some(pointer) = e.device().interface::<Pointer>() {
devices
.pointer
.write()
.unwrap()
.replace((device.device().clone(), pointer));
}
if let Some(keyboard) = e.device().interface::<Keyboard>() {
devices
.keyboard
.write()
.unwrap()
.replace((device.device().clone(), keyboard));
}
if let Some(scroll) = e.device().interface::<Scroll>() {
devices
.scroll
.write()
.unwrap()
.replace((device.device().clone(), scroll));
}
if let Some(button) = e.device().interface::<Button>() {
devices
.button
.write()
.unwrap()
.replace((device.device().clone(), button));
}
}
EiEvent::DeviceRemoved(e) => {
log::debug!("device removed: {:?}", e.device().device_type());
}
EiEvent::DevicePaused(e) => {
log::debug!("device paused: {:?}", e.device().device_type());
}
EiEvent::DeviceResumed(e) => {
log::debug!("device resumed: {:?}", e.device().device_type());
e.device().device().start_emulating(0, 0);
}
EiEvent::KeyboardModifiers(e) => {
log::debug!("modifiers: {e:?}");
}
// only for receiver context
// EiEvent::Frame(_) => { },
// EiEvent::DeviceStartEmulating(_) => { },
// EiEvent::DeviceStopEmulating(_) => { },
// EiEvent::PointerMotion(_) => { },
// EiEvent::PointerMotionAbsolute(_) => { },
// EiEvent::Button(_) => { },
// EiEvent::ScrollDelta(_) => { },
// EiEvent::ScrollStop(_) => { },
// EiEvent::ScrollCancel(_) => { },
// EiEvent::ScrollDiscrete(_) => { },
// EiEvent::KeyboardKey(_) => { },
// EiEvent::TouchDown(_) => { },
// EiEvent::TouchUp(_) => { },
// EiEvent::TouchMotion(_) => { },
_ => unreachable!("unexpected ei event"),
}
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
Ok(())
}

View File

@@ -1,18 +1,18 @@
use crate::client::{ClientEvent, ClientHandle}; use super::{error::EmulationError, EmulationHandle, InputEmulation};
use crate::emulate::InputEmulation;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint}; use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{ use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit, CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use std::time::Duration; use std::time::Duration;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
@@ -53,11 +53,9 @@ impl IndexMut<CGMouseButton> for ButtonState {
unsafe impl Send for MacOSEmulation {} unsafe impl Send for MacOSEmulation {}
impl MacOSEmulation { impl MacOSEmulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, MacOSEmulationCreationError> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) { let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
Ok(e) => e, .map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?;
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState { let button_state = ButtonState {
left: false, left: false,
right: false, right: false,
@@ -109,14 +107,14 @@ fn key_event(event_source: CGEventSource, key: u16, state: u8) {
#[async_trait] #[async_trait]
impl InputEmulation for MacOSEmulation { impl InputEmulation for MacOSEmulation {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) { async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { PointerEvent::Motion { time: _, dx, dy } => {
time: _,
relative_x,
relative_y,
} => {
// FIXME secondary displays? // FIXME secondary displays?
let (min_x, min_y, max_x, max_y) = unsafe { let (min_x, min_y, max_x, max_y) = unsafe {
let display = CGMainDisplayID(); let display = CGMainDisplayID();
@@ -131,12 +129,12 @@ impl InputEmulation for MacOSEmulation {
Some(l) => l, Some(l) => l,
None => { None => {
log::warn!("could not get mouse location!"); log::warn!("could not get mouse location!");
return; return Ok(());
} }
}; };
mouse_location.x = (mouse_location.x + relative_x).clamp(min_x, max_x - 1.); mouse_location.x = (mouse_location.x + dx).clamp(min_x, max_x - 1.);
mouse_location.y = (mouse_location.y + relative_y).clamp(min_y, max_y - 1.); mouse_location.y = (mouse_location.y + dy).clamp(min_y, max_y - 1.);
let mut event_type = CGEventType::MouseMoved; let mut event_type = CGEventType::MouseMoved;
if self.button_state.left { if self.button_state.left {
@@ -155,17 +153,11 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(_) => { Err(_) => {
log::warn!("mouse event creation failed!"); log::warn!("mouse event creation failed!");
return; return Ok(());
} }
}; };
event.set_integer_value_field( event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
EventField::MOUSE_EVENT_DELTA_X, event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
relative_x as i64,
);
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_Y,
relative_y as i64,
);
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
} }
PointerEvent::Button { PointerEvent::Button {
@@ -174,27 +166,27 @@ impl InputEmulation for MacOSEmulation {
state, state,
} => { } => {
let (event_type, mouse_button) = match (button, state) { let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => { (b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left) (CGEventType::LeftMouseDown, CGMouseButton::Left)
} }
(b, 0) if b == crate::event::BTN_LEFT => { (b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left) (CGEventType::LeftMouseUp, CGMouseButton::Left)
} }
(b, 1) if b == crate::event::BTN_RIGHT => { (b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right) (CGEventType::RightMouseDown, CGMouseButton::Right)
} }
(b, 0) if b == crate::event::BTN_RIGHT => { (b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right) (CGEventType::RightMouseUp, CGMouseButton::Right)
} }
(b, 1) if b == crate::event::BTN_MIDDLE => { (b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center) (CGEventType::OtherMouseDown, CGMouseButton::Center)
} }
(b, 0) if b == crate::event::BTN_MIDDLE => { (b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center) (CGEventType::OtherMouseUp, CGMouseButton::Center)
} }
_ => { _ => {
log::warn!("invalid button event: {button},{state}"); log::warn!("invalid button event: {button},{state}");
return; return Ok(());
} }
}; };
// store button state // store button state
@@ -210,7 +202,7 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(()) => { Err(()) => {
log::warn!("mouse event creation failed!"); log::warn!("mouse event creation failed!");
return; return Ok(());
} }
}; };
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
@@ -226,7 +218,7 @@ impl InputEmulation for MacOSEmulation {
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x) 1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => { _ => {
log::warn!("invalid scroll event: {axis}, {value}"); log::warn!("invalid scroll event: {axis}, {value}");
return; return Ok(());
} }
}; };
let event = match CGEvent::new_scroll_event( let event = match CGEvent::new_scroll_event(
@@ -240,7 +232,7 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(()) => { Err(()) => {
log::warn!("scroll event creation failed!"); log::warn!("scroll event creation failed!");
return; return Ok(());
} }
}; };
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
@@ -251,7 +243,7 @@ impl InputEmulation for MacOSEmulation {
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x) 1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => { _ => {
log::warn!("invalid scroll event: {axis}, {value}"); log::warn!("invalid scroll event: {axis}, {value}");
return; return Ok(());
} }
}; };
let event = match CGEvent::new_scroll_event( let event = match CGEvent::new_scroll_event(
@@ -265,7 +257,7 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(()) => { Err(()) => {
log::warn!("scroll event creation failed!"); log::warn!("scroll event creation failed!");
return; return Ok(());
} }
}; };
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
@@ -282,7 +274,7 @@ impl InputEmulation for MacOSEmulation {
Ok(k) => k.mac as CGKeyCode, Ok(k) => k.mac as CGKeyCode,
Err(_) => { Err(_) => {
log::warn!("unable to map key event"); log::warn!("unable to map key event");
return; return Ok(());
} }
}; };
match state { match state {
@@ -296,9 +288,11 @@ impl InputEmulation for MacOSEmulation {
}, },
_ => (), _ => (),
} }
// FIXME
Ok(())
} }
async fn notify(&mut self, _client_event: ClientEvent) {} async fn create(&mut self, _handle: EmulationHandle) {}
async fn destroy(&mut self) {} async fn destroy(&mut self, _handle: EmulationHandle) {}
} }

View File

@@ -1,9 +1,9 @@
use crate::{ use super::error::{EmulationError, WindowsEmulationCreationError};
emulate::InputEmulation, use input_event::{
event::{KeyboardEvent, PointerEvent}, scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
scancode, BTN_RIGHT,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use std::ops::BitOrAssign; use std::ops::BitOrAssign;
use std::time::Duration; use std::time::Duration;
@@ -19,11 +19,7 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
}; };
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2}; use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use crate::event::{BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT}; use super::{EmulationHandle, InputEmulation};
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
@@ -33,22 +29,18 @@ pub struct WindowsEmulation {
} }
impl WindowsEmulation { impl WindowsEmulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, WindowsEmulationCreationError> {
Ok(Self { repeat_task: None }) Ok(Self { repeat_task: None })
} }
} }
#[async_trait] #[async_trait]
impl InputEmulation for WindowsEmulation { impl InputEmulation for WindowsEmulation {
async fn consume(&mut self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { PointerEvent::Motion { time: _, dx, dy } => {
time: _, rel_mouse(dx as i32, dy as i32);
relative_x,
relative_y,
} => {
rel_mouse(relative_x as i32, relative_y as i32);
} }
PointerEvent::Button { PointerEvent::Button {
time: _, time: _,
@@ -81,13 +73,13 @@ impl InputEmulation for WindowsEmulation {
}, },
_ => {} _ => {}
} }
// FIXME
Ok(())
} }
async fn notify(&mut self, _: ClientEvent) { async fn create(&mut self, _handle: EmulationHandle) {}
// nothing to do
}
async fn destroy(&mut self) {} async fn destroy(&mut self, _handle: EmulationHandle) {}
} }
impl WindowsEmulation { impl WindowsEmulation {

View File

@@ -1,5 +1,6 @@
use crate::client::{ClientEvent, ClientHandle}; use crate::error::EmulationError;
use crate::emulate::InputEmulation;
use super::{error::WlrootsEmulationCreationError, InputEmulation};
use async_trait::async_trait; use async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
@@ -7,7 +8,6 @@ use std::os::fd::{AsFd, OwnedFd};
use wayland_client::backend::WaylandError; use wayland_client::backend::WaylandError;
use wayland_client::WEnum; use wayland_client::WEnum;
use anyhow::{anyhow, Result};
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState}; use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
@@ -28,11 +28,14 @@ use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle, Connection, Dispatch, EventQueue, QueueHandle,
}; };
use crate::event::{Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent};
use super::error::WaylandBindError;
use super::EmulationHandle;
struct State { struct State {
keymap: Option<(u32, OwnedFd, u32)>, keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<ClientHandle, VirtualInput>, input_for_client: HashMap<EmulationHandle, VirtualInput>,
seat: wl_seat::WlSeat, seat: wl_seat::WlSeat,
qh: QueueHandle<Self>, qh: QueueHandle<Self>,
vpm: VpManager, vpm: VpManager,
@@ -47,20 +50,23 @@ pub(crate) struct WlrootsEmulation {
} }
impl WlrootsEmulation { impl WlrootsEmulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, WlrootsEmulationCreationError> {
let conn = Connection::connect_to_env()?; let conn = Connection::connect_to_env()?;
let (globals, queue) = registry_queue_init::<State>(&conn)?; let (globals, queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle(); let qh = queue.handle();
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) { let seat: wl_seat::WlSeat = globals
Ok(wl_seat) => wl_seat, .bind(&qh, 7..=8, ())
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")), .map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
};
let vpm: VpManager = globals.bind(&qh, 1..=1, ())?; let vpm: VpManager = globals
let vkm: VkManager = globals.bind(&qh, 1..=1, ())?; .bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "wlr-virtual-pointer-unstable-v1"))?;
let vkm: VkManager = globals
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "virtual-keyboard-unstable-v1"))?;
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new(); let input_for_client: HashMap<EmulationHandle, VirtualInput> = HashMap::new();
let mut emulate = WlrootsEmulation { let mut emulate = WlrootsEmulation {
last_flush_failed: false, last_flush_failed: false,
@@ -75,7 +81,7 @@ impl WlrootsEmulation {
queue, queue,
}; };
while emulate.state.keymap.is_none() { while emulate.state.keymap.is_none() {
emulate.queue.blocking_dispatch(&mut emulate.state).unwrap(); emulate.queue.blocking_dispatch(&mut emulate.state)?;
} }
// let fd = unsafe { &File::from_raw_fd(emulate.state.keymap.unwrap().1.as_raw_fd()) }; // let fd = unsafe { &File::from_raw_fd(emulate.state.keymap.unwrap().1.as_raw_fd()) };
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() }; // let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
@@ -85,7 +91,7 @@ impl WlrootsEmulation {
} }
impl State { impl State {
fn add_client(&mut self, client: ClientHandle) { fn add_client(&mut self, client: EmulationHandle) {
let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ()); let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ()); let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
@@ -100,56 +106,65 @@ impl State {
self.input_for_client.insert(client, vinput); self.input_for_client.insert(client, vinput);
} }
fn destroy_client(&mut self, handle: EmulationHandle) {
if let Some(input) = self.input_for_client.remove(&handle) {
input.pointer.destroy();
input.keyboard.destroy();
}
}
} }
#[async_trait] #[async_trait]
impl InputEmulation for WlrootsEmulation { impl InputEmulation for WlrootsEmulation {
async fn consume(&mut self, event: Event, client_handle: ClientHandle) { async fn consume(
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) { &mut self,
event: Event,
handle: EmulationHandle,
) -> Result<(), EmulationError> {
if let Some(virtual_input) = self.state.input_for_client.get(&handle) {
if self.last_flush_failed { if self.last_flush_failed {
if let Err(WaylandError::Io(e)) = self.queue.flush() { match self.queue.flush() {
if e.kind() == io::ErrorKind::WouldBlock { Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
/* /*
* outgoing buffer is full - sending more events * outgoing buffer is full - sending more events
* will overwhelm the output buffer and leave the * will overwhelm the output buffer and leave the
* wayland connection in a broken state * wayland connection in a broken state
*/ */
log::warn!( log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
"can't keep up, discarding event: ({client_handle}) - {event:?}" return Ok(());
);
return;
} }
_ => {}
} }
} }
virtual_input.consume_event(event).unwrap(); virtual_input
.consume_event(event)
.unwrap_or_else(|_| panic!("failed to convert event: {event:?}"));
match self.queue.flush() { match self.queue.flush() {
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => { Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
self.last_flush_failed = true; self.last_flush_failed = true;
log::warn!("can't keep up, retrying ..."); log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
}
Err(WaylandError::Io(e)) => {
log::error!("{e}")
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland protocol violation: {e}")
}
Ok(()) => {
self.last_flush_failed = false;
} }
Err(WaylandError::Protocol(e)) => panic!("wayland protocol violation: {e}"),
Ok(()) => self.last_flush_failed = false,
Err(e) => Err(e)?,
} }
} }
Ok(())
} }
async fn notify(&mut self, client_event: ClientEvent) { async fn create(&mut self, handle: EmulationHandle) {
if let ClientEvent::Create(client, _) = client_event { self.state.add_client(handle);
self.state.add_client(client); if let Err(e) = self.queue.flush() {
if let Err(e) = self.queue.flush() { log::error!("{}", e);
log::error!("{}", e); }
} }
async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() {
log::error!("{}", e);
} }
} }
async fn destroy(&mut self) {}
} }
struct VirtualInput { struct VirtualInput {
@@ -162,11 +177,7 @@ impl VirtualInput {
match event { match event {
Event::Pointer(e) => { Event::Pointer(e) => {
match e { match e {
PointerEvent::Motion { PointerEvent::Motion { time, dx, dy } => self.pointer.motion(time, dx, dy),
time,
relative_x,
relative_y,
} => self.pointer.motion(time, relative_x, relative_y),
PointerEvent::Button { PointerEvent::Button {
time, time,
button, button,
@@ -182,7 +193,8 @@ impl VirtualInput {
} }
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?; let axis: Axis = (axis as u32).try_into()?;
self.pointer.axis(0, axis, (value / 15) as f64); self.pointer
.axis_discrete(0, axis, value as f64 / 6., value / 120);
self.pointer.frame(); self.pointer.frame();
} }
PointerEvent::Frame {} => self.pointer.frame(), PointerEvent::Frame {} => self.pointer.frame(),

View File

@@ -1,4 +1,3 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use std::ptr; use std::ptr;
use x11::{ use x11::{
@@ -6,14 +5,14 @@ use x11::{
xtest, xtest,
}; };
use crate::{ use input_event::{
client::ClientHandle, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
emulate::InputEmulation,
event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
},
}; };
use crate::error::EmulationError;
use super::{error::X11EmulationCreationError, EmulationHandle, InputEmulation};
pub struct X11Emulation { pub struct X11Emulation {
display: *mut xlib::Display, display: *mut xlib::Display,
} }
@@ -21,11 +20,11 @@ pub struct X11Emulation {
unsafe impl Send for X11Emulation {} unsafe impl Send for X11Emulation {}
impl X11Emulation { impl X11Emulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => { d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(anyhow!("could not open display")) Err(X11EmulationCreationError::OpenDisplay)
} }
display => Ok(display), display => Ok(display),
} }
@@ -101,15 +100,11 @@ impl Drop for X11Emulation {
#[async_trait] #[async_trait]
impl InputEmulation for X11Emulation { impl InputEmulation for X11Emulation {
async fn consume(&mut self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { PointerEvent::Motion { time: _, dx, dy } => {
time: _, self.relative_motion(dx as i32, dy as i32);
relative_x,
relative_y,
} => {
self.relative_motion(relative_x as i32, relative_y as i32);
} }
PointerEvent::Button { PointerEvent::Button {
time: _, time: _,
@@ -142,11 +137,15 @@ impl InputEmulation for X11Emulation {
unsafe { unsafe {
xlib::XFlush(self.display); xlib::XFlush(self.display);
} }
// FIXME
Ok(())
} }
async fn notify(&mut self, _: crate::client::ClientEvent) { async fn create(&mut self, _: EmulationHandle) {
// for our purposes it does not matter what client sent the event // for our purposes it does not matter what client sent the event
} }
async fn destroy(&mut self) {} async fn destroy(&mut self, _: EmulationHandle) {
// for our purposes it does not matter what client sent the event
}
} }

View File

@@ -1,29 +1,30 @@
use anyhow::Result;
use ashpd::{ use ashpd::{
desktop::{ desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop}, remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
ResponseError, Session, ResponseError, Session,
}, },
zbus::AsyncDrop,
WindowIdentifier, WindowIdentifier,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use crate::{ use futures::FutureExt;
client::ClientEvent, use input_event::{
emulate::InputEmulation, Event::{Keyboard, Pointer},
event::{ KeyboardEvent, PointerEvent,
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
},
}; };
use crate::error::EmulationError;
use super::{error::XdpEmulationCreationError, EmulationHandle, InputEmulation};
pub struct DesktopPortalEmulation<'a> { pub struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>, proxy: RemoteDesktop<'a>,
session: Session<'a>, session: Session<'a>,
} }
impl<'a> DesktopPortalEmulation<'a> { impl<'a> DesktopPortalEmulation<'a> {
pub async fn new() -> Result<DesktopPortalEmulation<'a>> { pub async fn new() -> Result<DesktopPortalEmulation<'a>, XdpEmulationCreationError> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ..."); log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?; let proxy = RemoteDesktop::new().await?;
@@ -53,6 +54,7 @@ impl<'a> DesktopPortalEmulation<'a> {
}; };
log::debug!("started session"); log::debug!("started session");
let session = session;
Ok(Self { proxy, session }) Ok(Self { proxy, session })
} }
@@ -60,21 +62,17 @@ impl<'a> DesktopPortalEmulation<'a> {
#[async_trait] #[async_trait]
impl<'a> InputEmulation for DesktopPortalEmulation<'a> { impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) { async fn consume(
&mut self,
event: input_event::Event,
_client: EmulationHandle,
) -> Result<(), EmulationError> {
match event { match event {
Pointer(p) => match p { Pointer(p) => match p {
PointerEvent::Motion { PointerEvent::Motion { time: _, dx, dy } => {
time: _, self.proxy
relative_x, .notify_pointer_motion(&self.session, dx, dy)
relative_y, .await?;
} => {
if let Err(e) = self
.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await
{
log::warn!("{e}");
}
} }
PointerEvent::Button { PointerEvent::Button {
time: _, time: _,
@@ -85,26 +83,18 @@ impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
0 => KeyState::Released, 0 => KeyState::Released,
_ => KeyState::Pressed, _ => KeyState::Pressed,
}; };
if let Err(e) = self self.proxy
.proxy
.notify_pointer_button(&self.session, button as i32, state) .notify_pointer_button(&self.session, button as i32, state)
.await .await?;
{
log::warn!("{e}");
}
} }
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = match axis { let axis = match axis {
0 => Axis::Vertical, 0 => Axis::Vertical,
_ => Axis::Horizontal, _ => Axis::Horizontal,
}; };
if let Err(e) = self self.proxy
.proxy
.notify_pointer_axis_discrete(&self.session, axis, value) .notify_pointer_axis_discrete(&self.session, axis, value)
.await .await?;
{
log::warn!("{e}");
}
} }
PointerEvent::Axis { PointerEvent::Axis {
time: _, time: _,
@@ -119,13 +109,9 @@ impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
Axis::Vertical => (0., value), Axis::Vertical => (0., value),
Axis::Horizontal => (value, 0.), Axis::Horizontal => (value, 0.),
}; };
if let Err(e) = self self.proxy
.proxy
.notify_pointer_axis(&self.session, dx, dy, true) .notify_pointer_axis(&self.session, dx, dy, true)
.await .await?;
{
log::warn!("{e}");
}
} }
PointerEvent::Frame {} => {} PointerEvent::Frame {} => {}
}, },
@@ -140,13 +126,9 @@ impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
0 => KeyState::Released, 0 => KeyState::Released,
_ => KeyState::Pressed, _ => KeyState::Pressed,
}; };
if let Err(e) = self self.proxy
.proxy
.notify_keyboard_keycode(&self.session, key as i32, state) .notify_keyboard_keycode(&self.session, key as i32, state)
.await .await?;
{
log::warn!("{e}");
}
} }
KeyboardEvent::Modifiers { .. } => { KeyboardEvent::Modifiers { .. } => {
// ignore // ignore
@@ -155,14 +137,28 @@ impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
} }
_ => {} _ => {}
} }
Ok(())
} }
async fn notify(&mut self, _client: ClientEvent) {} async fn create(&mut self, _client: EmulationHandle) {}
async fn destroy(&mut self, _client: EmulationHandle) {}
}
async fn destroy(&mut self) { impl<'a> AsyncDrop for DesktopPortalEmulation<'a> {
log::debug!("closing remote desktop session"); #[doc = r" Perform the async cleanup."]
if let Err(e) = self.session.close().await { #[must_use]
log::error!("failed to close remote desktop session: {e}"); #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>(
self,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
Self: 'async_trait,
{
async move {
let _ = self.session.close().await;
} }
.boxed()
} }
} }

14
input-event/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "input-event"
description = "cross-platform input-event types for input-capture / input-emulation"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
[dependencies]
futures-core = "0.3.30"
log = "0.4.22"
num_enum = "0.7.2"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0.61"

17
input-event/src/error.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::array::TryFromSliceError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProtocolError {
#[error(transparent)]
MissingData(#[from] TryFromSliceError),
#[error("invalid event id: `{0}`")]
InvalidEventId(u8),
#[error("invalid pointer event type: `{0}`")]
InvalidPointerEventId(u8),
#[error("invalid keyboard event type: `{0}`")]
InvalidKeyboardEventId(u8),
#[error("expected data at idx `{0:?}`")]
Data(String),
}

143
input-event/src/lib.rs Normal file
View File

@@ -0,0 +1,143 @@
pub use error::ProtocolError;
use std::fmt::{self, Display};
pub mod error;
pub mod proto;
pub mod scancode;
// FIXME
pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111;
pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PointerEvent {
/// relative motion event
Motion { time: u32, dx: f64, dy: f64 },
/// mouse button event
Button { time: u32, button: u32, state: u32 },
/// axis event, scroll event for touchpads
Axis { time: u32, axis: u8, value: f64 },
/// discrete axis event, scroll event for mice - 120 = one scroll tick
AxisDiscrete120 { axis: u8, value: i32 },
/// frame event
Frame {},
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum KeyboardEvent {
/// a key press / release event
Key { time: u32, key: u32, state: u8 },
/// modifiers changed state
Modifiers {
mods_depressed: u32,
mods_latched: u32,
mods_locked: u32,
group: u32,
},
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Event {
/// pointer event (motion / button / axis)
Pointer(PointerEvent),
/// keyboard events (key / modifiers)
Keyboard(KeyboardEvent),
/// enter event: request to enter a client.
/// The client must release the pointer if it is grabbed
/// and reply with a leave event, as soon as its ready to
/// receive events
Enter(),
/// leave event: this client is now ready to receive events and will
/// not send any events after until it sends an enter event
Leave(),
/// ping a client, to see if it is still alive. A client that does
/// not respond with a pong event will be assumed to be offline.
Ping(),
/// response to a ping event: this event signals that a client
/// is still alive but must otherwise be ignored
Pong(),
/// explicit disconnect request. The client will no longer
/// send events until the next Enter event. All of its keys should be released.
Disconnect(),
}
impl Display for PointerEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PointerEvent::Motion { time: _, dx, dy } => write!(f, "motion({dx},{dy})"),
PointerEvent::Button {
time: _,
button,
state,
} => {
let str = match *button {
BTN_LEFT => Some("left"),
BTN_RIGHT => Some("right"),
BTN_MIDDLE => Some("middle"),
BTN_FORWARD => Some("forward"),
BTN_BACK => Some("back"),
_ => None,
};
if let Some(button) = str {
write!(f, "button({button}, {state})")
} else {
write!(f, "button({button}, {state}")
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => write!(f, "scroll({axis}, {value})"),
PointerEvent::AxisDiscrete120 { axis, value } => {
write!(f, "scroll-120 ({axis}, {value})")
}
PointerEvent::Frame {} => write!(f, "frame()"),
}
}
}
impl Display for KeyboardEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let scan = scancode::Linux::try_from(*key);
if let Ok(scan) = scan {
write!(f, "key({scan:?}, {state})")
} else {
write!(f, "key({key}, {state})")
}
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => write!(
f,
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
),
}
}
}
impl Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k),
Event::Enter() => write!(f, "enter"),
Event::Leave() => write!(f, "leave"),
Event::Ping() => write!(f, "ping"),
Event::Pong() => write!(f, "pong"),
Event::Disconnect() => write!(f, "disconnect"),
}
}
}

307
input-event/src/proto.rs Normal file
View File

@@ -0,0 +1,307 @@
use std::{fmt::Debug, slice::SliceIndex};
use crate::ProtocolError;
use super::{Event, KeyboardEvent, PointerEvent};
enum PointerEventType {
Motion,
Button,
Axis,
AxisDiscrete120,
Frame,
}
enum KeyboardEventType {
Key,
Modifiers,
}
enum EventType {
Pointer,
Keyboard,
Enter,
Leave,
Ping,
Pong,
Disconnect,
}
impl Event {
fn event_type(&self) -> EventType {
match self {
Self::Pointer(_) => EventType::Pointer,
Self::Keyboard(_) => EventType::Keyboard,
Self::Enter() => EventType::Enter,
Self::Leave() => EventType::Leave,
Self::Ping() => EventType::Ping,
Self::Pong() => EventType::Pong,
Self::Disconnect() => EventType::Disconnect,
}
}
}
impl PointerEvent {
fn event_type(&self) -> PointerEventType {
match self {
Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::Axis,
Self::AxisDiscrete120 { .. } => PointerEventType::AxisDiscrete120,
Self::Frame { .. } => PointerEventType::Frame,
}
}
}
impl KeyboardEvent {
fn event_type(&self) -> KeyboardEventType {
match self {
KeyboardEvent::Key { .. } => KeyboardEventType::Key,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
}
}
}
impl TryFrom<u8> for PointerEventType {
type Error = ProtocolError;
fn try_from(value: u8) -> Result<Self, ProtocolError> {
match value {
x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::AxisDiscrete120 as u8 => Ok(Self::AxisDiscrete120),
x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(ProtocolError::InvalidPointerEventId(value)),
}
}
}
impl TryFrom<u8> for KeyboardEventType {
type Error = ProtocolError;
fn try_from(value: u8) -> Result<Self, ProtocolError> {
match value {
x if x == Self::Key as u8 => Ok(Self::Key),
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
_ => Err(ProtocolError::InvalidKeyboardEventId(value)),
}
}
}
impl From<&Event> for Vec<u8> {
fn from(event: &Event) -> Self {
let event_id = vec![event.event_type() as u8];
let event_data = match event {
Event::Pointer(p) => p.into(),
Event::Keyboard(k) => k.into(),
Event::Enter() => vec![],
Event::Leave() => vec![],
Event::Ping() => vec![],
Event::Pong() => vec![],
Event::Disconnect() => vec![],
};
[event_id, event_data].concat()
}
}
impl TryFrom<Vec<u8>> for Event {
type Error = ProtocolError;
fn try_from(value: Vec<u8>) -> Result<Self, ProtocolError> {
let event_id = u8::from_be_bytes(value[..1].try_into()?);
match event_id {
i if i == (EventType::Pointer as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
i if i == (EventType::Disconnect as u8) => Ok(Event::Disconnect()),
_ => Err(ProtocolError::InvalidEventId(event_id)),
}
}
}
impl From<&PointerEvent> for Vec<u8> {
fn from(event: &PointerEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
PointerEvent::Motion { time, dx, dy } => {
let time = time.to_be_bytes();
let dx = dx.to_be_bytes();
let dy = dy.to_be_bytes();
[&time[..], &dx[..], &dy[..]].concat()
}
PointerEvent::Button {
time,
button,
state,
} => {
let time = time.to_be_bytes();
let button = button.to_be_bytes();
let state = state.to_be_bytes();
[&time[..], &button[..], &state[..]].concat()
}
PointerEvent::Axis { time, axis, value } => {
let time = time.to_be_bytes();
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
[&time[..], &axis[..], &value[..]].concat()
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
[&axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => {
vec![]
}
};
[id, data].concat()
}
}
fn decode_u8<I>(data: &[u8], idx: I) -> Result<u8, ProtocolError>
where
I: SliceIndex<[u8], Output = [u8]> + Debug + Clone,
{
let data = data
.get(idx.clone())
.ok_or(ProtocolError::Data(format!("{:?}", idx)))?;
Ok(u8::from_be_bytes(data.try_into()?))
}
fn decode_u32<I>(data: &[u8], idx: I) -> Result<u32, ProtocolError>
where
I: SliceIndex<[u8], Output = [u8]> + Debug + Clone,
{
let data = data
.get(idx.clone())
.ok_or(ProtocolError::Data(format!("{:?}", idx)))?;
Ok(u32::from_be_bytes(data.try_into()?))
}
fn decode_i32<I>(data: &[u8], idx: I) -> Result<i32, ProtocolError>
where
I: SliceIndex<[u8], Output = [u8]> + Debug + Clone,
{
let data = data
.get(idx.clone())
.ok_or(ProtocolError::Data(format!("{:?}", idx)))?;
Ok(i32::from_be_bytes(data.try_into()?))
}
fn decode_f64<I>(data: &[u8], idx: I) -> Result<f64, ProtocolError>
where
I: SliceIndex<[u8], Output = [u8]> + Debug + Clone,
{
let data = data
.get(idx.clone())
.ok_or(ProtocolError::Data(format!("{:?}", idx)))?;
Ok(f64::from_be_bytes(data.try_into()?))
}
impl TryFrom<Vec<u8>> for PointerEvent {
type Error = ProtocolError;
fn try_from(data: Vec<u8>) -> Result<Self, ProtocolError> {
match data.get(1) {
Some(id) => match id.to_owned().try_into()? {
PointerEventType::Motion => {
let time = decode_u32(&data, 2..6)?;
let dx = decode_f64(&data, 6..14)?;
let dy = decode_f64(&data, 14..22)?;
Ok(Self::Motion { time, dx, dy })
}
PointerEventType::Button => {
let time = decode_u32(&data, 2..6)?;
let button = decode_u32(&data, 6..10)?;
let state = decode_u32(&data, 10..14)?;
Ok(Self::Button {
time,
button,
state,
})
}
PointerEventType::Axis => {
let time = decode_u32(&data, 2..6)?;
let axis = decode_u8(&data, 6..7)?;
let value = decode_f64(&data, 7..15)?;
Ok(Self::Axis { time, axis, value })
}
PointerEventType::AxisDiscrete120 => {
let axis = decode_u8(&data, 2..3)?;
let value = decode_i32(&data, 3..7)?;
Ok(Self::AxisDiscrete120 { axis, value })
}
PointerEventType::Frame => Ok(Self::Frame {}),
},
None => Err(ProtocolError::Data("0".to_string())),
}
}
}
impl From<&KeyboardEvent> for Vec<u8> {
fn from(event: &KeyboardEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
KeyboardEvent::Key { time, key, state } => {
let time = time.to_be_bytes();
let key = key.to_be_bytes();
let state = state.to_be_bytes();
[&time[..], &key[..], &state[..]].concat()
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => {
let mods_depressed = mods_depressed.to_be_bytes();
let mods_latched = mods_latched.to_be_bytes();
let mods_locked = mods_locked.to_be_bytes();
let group = group.to_be_bytes();
[
&mods_depressed[..],
&mods_latched[..],
&mods_locked[..],
&group[..],
]
.concat()
}
};
[id, data].concat()
}
}
impl TryFrom<Vec<u8>> for KeyboardEvent {
type Error = ProtocolError;
fn try_from(data: Vec<u8>) -> Result<Self, ProtocolError> {
match data.get(1) {
Some(id) => match id.to_owned().try_into()? {
KeyboardEventType::Key => {
let time = decode_u32(&data, 2..6)?;
let key = decode_u32(&data, 6..10)?;
let state = decode_u8(&data, 10..11)?;
Ok(KeyboardEvent::Key { time, key, state })
}
KeyboardEventType::Modifiers => {
let mods_depressed = decode_u32(&data, 2..6)?;
let mods_latched = decode_u32(&data, 6..10)?;
let mods_locked = decode_u32(&data, 10..14)?;
let group = decode_u32(&data, 14..18)?;
Ok(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
})
}
},
None => Err(ProtocolError::Data("0".to_string())),
}
}
}

View File

@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
/* /*
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input * https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
* https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf
* https://kbd-project.org/docs/scancodes/scancodes-1.html
*/ */
#[repr(u32)] #[repr(u32)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)] #[derive(Debug, Clone, Copy, TryFromPrimitive)]
@@ -120,15 +122,15 @@ pub enum Windows {
KeyF21 = 0x006C, KeyF21 = 0x006C,
KeyF22 = 0x006D, KeyF22 = 0x006D,
KeyF23 = 0x006E, KeyF23 = 0x006E,
KeyF24 = 0x0076, KeyF24 = 0x0076, // KeyLANG5
KeypadComma = 0x007E, KeypadComma = 0x007E,
KeyInternational1 = 0x0073, KeyInternational1 = 0x0073,
KeyInternational2 = 0x0070, KeyInternational2 = 0x0070,
KeyInternational3 = 0x007D, KeyInternational3 = 0x007D, // typo in doc -> its Int'l 3 not Int'l 2
#[allow(dead_code)] #[allow(dead_code)]
KeyInternational4 = 0x0079, // FIXME unused KeyInternational4 = 0x0079,
#[allow(dead_code)] #[allow(dead_code)]
KeyInternational5 = 0x007B, // FIXME unused KeyInternational5 = 0x007B,
// KeyInternational6 = 0x005C, // KeyInternational6 = 0x005C,
KeyLANG1 = 0x0072, KeyLANG1 = 0x0072,
KeyLANG2 = 0x0071, KeyLANG2 = 0x0071,
@@ -141,6 +143,7 @@ pub enum Windows {
KeyLeftGUI = 0xE05B, KeyLeftGUI = 0xE05B,
KeyRightCtrl = 0xE01D, KeyRightCtrl = 0xE01D,
KeyRightShift = 0x0036, KeyRightShift = 0x0036,
KeyFakeRightShift = 0xE036,
KeyRightAlt = 0xE038, KeyRightAlt = 0xE038,
KeyRightGUI = 0xE05C, KeyRightGUI = 0xE05C,
KeyScanNextTrack = 0xE019, KeyScanNextTrack = 0xE019,
@@ -293,7 +296,7 @@ pub enum Linux {
KeyPause = 119, KeyPause = 119,
KeyScale = 120, /* AL Compiz Scale (Expose) */ KeyScale = 120, /* AL Compiz Scale (Expose) */
KeyKpcomma = 121, KeyKpcomma = 121,
KeyHangeul = 122, KeyHanguel = 122,
// KEY_HANGUEL = KeyHangeul, // KEY_HANGUEL = KeyHangeul,
KeyHanja = 123, KeyHanja = 123,
KeyYen = 124, KeyYen = 124,
@@ -518,16 +521,16 @@ impl TryFrom<Linux> for Windows {
Linux::KeyKp3 => Ok(Self::Keypad3PageDn), Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
Linux::KeyKp0 => Ok(Self::Keypad0Insert), Linux::KeyKp0 => Ok(Self::Keypad0Insert),
Linux::KeyKpDot => Ok(Self::KeypadDot), Linux::KeyKpDot => Ok(Self::KeypadDot),
Linux::KeyZenkakuhankaku => Ok(Self::KeyLANG1), // TODO unsure Linux::KeyZenkakuhankaku => Ok(Self::KeyF24), // KeyLANG5
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
Linux::KeyF11 => Ok(Self::KeyF11), Linux::KeyF11 => Ok(Self::KeyF11),
Linux::KeyF12 => Ok(Self::KeyF12), Linux::KeyF12 => Ok(Self::KeyF12),
Linux::KeyRo => Ok(Self::ErrorRollOver), // TODO unsure Linux::KeyRo => Ok(Self::KeyInternational1),
Linux::KeyKatakana => Ok(Self::KeyLANG1), // TODO unsure Linux::KeyKatakana => Ok(Self::KeyLANG3),
Linux::KeyHiragana => Ok(Self::KeyLANG2), // TODO unsure Linux::KeyHiragana => Ok(Self::KeyLANG4),
Linux::KeyHenkan => Ok(Self::KeyLANG3), // TODO unsure Linux::KeyHenkan => Ok(Self::KeyInternational4),
Linux::KeyKatakanahiragana => Ok(Self::KeyLANG4), // TODO unsure Linux::KeyKatakanahiragana => Ok(Self::KeyInternational2),
Linux::KeyMuhenkan => Ok(Self::KeyLANG4), // TODO unsure Linux::KeyMuhenkan => Ok(Self::KeyInternational5),
Linux::KeyKpJpComma => Ok(Self::KeypadComma), Linux::KeyKpJpComma => Ok(Self::KeypadComma),
Linux::KeyKpEnter => Ok(Self::KeypadEnter), Linux::KeyKpEnter => Ok(Self::KeypadEnter),
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl), Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
@@ -555,9 +558,9 @@ impl TryFrom<Linux> for Windows {
Linux::KeyPause => Ok(Self::KeyPause), Linux::KeyPause => Ok(Self::KeyPause),
Linux::KeyScale => Err(()), // TODO Linux::KeyScale => Err(()), // TODO
Linux::KeyKpcomma => Ok(Self::KeypadComma), Linux::KeyKpcomma => Ok(Self::KeypadComma),
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure Linux::KeyHanguel => Ok(Self::KeyLANG1), // FIXME should be 00F2?
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure Linux::KeyHanja => Ok(Self::KeyLANG2), // FIXME should be 00F1?
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure Linux::KeyYen => Ok(Self::KeyInternational3),
Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI), Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI),
Linux::KeyRightmeta => Ok(Self::KeyRightGUI), Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
Linux::KeyCompose => Ok(Self::KeyApplication), Linux::KeyCompose => Ok(Self::KeyApplication),
@@ -807,21 +810,22 @@ impl TryFrom<Windows> for Linux {
Windows::KeyF23 => Ok(Self::KeyF23), Windows::KeyF23 => Ok(Self::KeyF23),
Windows::KeyF24 => Ok(Self::KeyF24), Windows::KeyF24 => Ok(Self::KeyF24),
Windows::KeypadComma => Ok(Self::KeyKpcomma), Windows::KeypadComma => Ok(Self::KeyKpcomma),
Windows::KeyInternational1 => Ok(Self::KeyHangeul), Windows::KeyInternational1 => Ok(Self::KeyRo),
Windows::KeyInternational2 => Ok(Self::KeyHanja), Windows::KeyInternational2 => Ok(Self::KeyKatakanahiragana),
Windows::KeyInternational3 => Ok(Self::KeyYen), Windows::KeyInternational3 => Ok(Self::KeyYen),
Windows::KeyInternational4 => Err(()), Windows::KeyInternational4 => Ok(Self::KeyHenkan),
Windows::KeyInternational5 => Err(()), Windows::KeyInternational5 => Ok(Self::KeyMuhenkan),
Windows::KeyLANG1 => Ok(Self::KeyKatakana), Windows::KeyLANG1 => Ok(Self::KeyHanguel),
Windows::KeyLANG2 => Ok(Self::KeyHiragana), Windows::KeyLANG2 => Ok(Self::KeyHanja),
Windows::KeyLANG3 => Ok(Self::KeyHenkan), Windows::KeyLANG3 => Ok(Self::KeyKatakana),
Windows::KeyLANG4 => Ok(Self::KeyKatakanahiragana), Windows::KeyLANG4 => Ok(Self::KeyHiragana),
Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl), Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Windows::KeyLeftShift => Ok(Self::KeyLeftShift), Windows::KeyLeftShift => Ok(Self::KeyLeftShift),
Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt), Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt),
Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta), Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta),
Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl), Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Windows::KeyRightShift => Ok(Self::KeyRightShift), Windows::KeyRightShift => Ok(Self::KeyRightShift),
Windows::KeyFakeRightShift => Ok(Self::KeyRightShift),
Windows::KeyRightAlt => Ok(Self::KeyRightalt), Windows::KeyRightAlt => Ok(Self::KeyRightalt),
Windows::KeyRightGUI => Ok(Self::KeyRightmeta), Windows::KeyRightGUI => Ok(Self::KeyRightmeta),
Windows::KeyScanNextTrack => Ok(Self::KeyNextsong), Windows::KeyScanNextTrack => Ok(Self::KeyNextsong),

View File

@@ -2,10 +2,14 @@
rustPlatform, rustPlatform,
lib, lib,
pkgs, pkgs,
}: }: let
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
pname = cargoToml.package.name;
version = cargoToml.package.version;
in
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage {
pname = "lan-mouse"; pname = pname;
version = "0.7.0"; version = version;
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
pkg-config pkg-config
@@ -23,7 +27,7 @@ rustPlatform.buildRustPackage {
]; ];
src = builtins.path { src = builtins.path {
name = "lan-mouse"; name = pname;
path = lib.cleanSource ../.; path = lib.cleanSource ../.;
}; };
@@ -38,7 +42,7 @@ rustPlatform.buildRustPackage {
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch. Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details). The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
''; '';
mainProgram = "lan-mouse"; mainProgram = pname;
platforms = platforms.all; platforms = platforms.all;
}; };
} }

View File

@@ -1,78 +0,0 @@
use std::io;
use futures_core::Stream;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
/// fallback input capture (does not produce events)
pub mod dummy;
pub async fn create() -> Box<dyn InputCapture<Item = io::Result<(ClientHandle, Event)>>> {
#[cfg(target_os = "macos")]
match macos::MacOSInputCapture::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("macos input capture not available: {e}"),
}
#[cfg(windows)]
match windows::WindowsInputCapture::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("windows input capture not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match libei::LibeiInputCapture::new().await {
Ok(p) => {
log::info!("using libei input capture");
return Box::new(p);
}
Err(e) => log::info!("libei input capture not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match wayland::WaylandInputCapture::new() {
Ok(p) => {
log::info!("using layer-shell input capture");
return Box::new(p);
}
Err(e) => log::info!("layer_shell input capture not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match x11::X11InputCapture::new() {
Ok(p) => {
log::info!("using x11 input capture");
return Box::new(p);
}
Err(e) => log::info!("x11 input capture not available: {e}"),
}
log::error!("falling back to dummy input capture");
Box::new(dummy::DummyInputCapture::new())
}
pub trait InputCapture: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
/// notify input capture of configuration changes
fn notify(&mut self, event: ClientEvent) -> io::Result<()>;
/// release mouse
fn release(&mut self) -> io::Result<()>;
}

View File

@@ -1,33 +0,0 @@
use crate::capture::InputCapture;
use crate::client::{ClientEvent, ClientHandle};
use crate::event::Event;
use anyhow::{anyhow, Result};
use futures_core::Stream;
use std::task::{Context, Poll};
use std::{io, pin::Pin};
pub struct MacOSInputCapture;
impl MacOSInputCapture {
pub fn new() -> Result<Self> {
Err(anyhow!("not yet implemented"))
}
}
impl Stream for MacOSInputCapture {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
}
}
impl InputCapture for MacOSInputCapture {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@@ -1,39 +0,0 @@
use anyhow::{anyhow, Result};
use std::io;
use std::task::Poll;
use futures_core::Stream;
use crate::capture::InputCapture;
use crate::event::Event;
use crate::client::{ClientEvent, ClientHandle};
pub struct X11InputCapture {}
impl X11InputCapture {
pub fn new() -> Result<Self> {
Err(anyhow!("not implemented"))
}
}
impl InputCapture for X11InputCapture {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Stream for X11InputCapture {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

@@ -1,8 +1,8 @@
use crate::capture; use crate::config::Config;
use crate::client::{ClientEvent, Position};
use crate::event::{Event, KeyboardEvent};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures::StreamExt; use futures::StreamExt;
use input_capture::{self, Position};
use input_event::{Event, KeyboardEvent};
use tokio::task::LocalSet; use tokio::task::LocalSet;
pub fn run() -> Result<()> { pub fn run() -> Result<()> {
@@ -12,17 +12,20 @@ pub fn run() -> Result<()> {
.enable_time() .enable_time()
.build()?; .build()?;
runtime.block_on(LocalSet::new().run_until(input_capture_test())) let config = Config::new()?;
runtime.block_on(LocalSet::new().run_until(input_capture_test(config)))
} }
async fn input_capture_test() -> Result<()> { async fn input_capture_test(config: Config) -> Result<()> {
log::info!("creating input capture"); log::info!("creating input capture");
let mut input_capture = capture::create().await; let backend = config.capture_backend.map(|b| b.into());
let mut input_capture = input_capture::create(backend).await?;
log::info!("creating clients"); log::info!("creating clients");
input_capture.notify(ClientEvent::Create(0, Position::Left))?; input_capture.create(0, Position::Left)?;
input_capture.notify(ClientEvent::Create(1, Position::Right))?; input_capture.create(1, Position::Right)?;
input_capture.notify(ClientEvent::Create(2, Position::Top))?; input_capture.create(2, Position::Top)?;
input_capture.notify(ClientEvent::Create(3, Position::Bottom))?; input_capture.create(3, Position::Bottom)?;
loop { loop {
let (client, event) = input_capture let (client, event) = input_capture
.next() .next()

View File

@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use slab::Slab; use slab::Slab;
use crate::config::DEFAULT_PORT; use crate::config::DEFAULT_PORT;
use input_capture;
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position { pub enum Position {
@@ -25,6 +26,17 @@ impl Default for Position {
} }
} }
impl From<Position> for input_capture::Position {
fn from(position: Position) -> input_capture::Position {
match position {
Position::Left => input_capture::Position::Left,
Position::Right => input_capture::Position::Right,
Position::Top => input_capture::Position::Top,
Position::Bottom => input_capture::Position::Bottom,
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct PositionParseError { pub struct PositionParseError {
string: String, string: String,
@@ -52,17 +64,6 @@ impl FromStr for Position {
} }
} }
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 { impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
@@ -102,6 +103,8 @@ pub struct ClientConfig {
pub port: u16, pub port: u16,
/// position of a client on screen /// position of a client on screen
pub pos: Position, pub pos: Position,
/// enter hook
pub cmd: Option<String>,
} }
impl Default for ClientConfig { impl Default for ClientConfig {
@@ -111,16 +114,11 @@ impl Default for ClientConfig {
hostname: Default::default(), hostname: Default::default(),
fix_ips: Default::default(), fix_ips: Default::default(),
pos: Default::default(), pos: Default::default(),
cmd: None,
} }
} }
} }
#[derive(Clone, Copy, Debug)]
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
}
pub type ClientHandle = u64; pub type ClientHandle = u64;
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]

View File

@@ -1,22 +1,28 @@
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
use std::fmt::Display;
use std::net::IpAddr; use std::net::IpAddr;
use std::{error::Error, fs}; use std::{error::Error, fs};
use toml; use toml;
use crate::client::Position; use crate::client::Position;
use crate::scancode;
use crate::scancode::Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift}; use input_event::scancode::{
self,
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
};
pub const DEFAULT_PORT: u16 = 4242; pub const DEFAULT_PORT: u16 = 4242;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml { pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub port: Option<u16>, pub port: Option<u16>,
pub frontend: Option<String>, pub frontend: Option<Frontend>,
pub release_bind: Option<Vec<scancode::Linux>>, pub release_bind: Option<Vec<scancode::Linux>>,
pub left: Option<TomlClient>, pub left: Option<TomlClient>,
pub right: Option<TomlClient>, pub right: Option<TomlClient>,
@@ -26,11 +32,13 @@ pub struct ConfigToml {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient { pub struct TomlClient {
pub capture_backend: Option<CaptureBackend>,
pub hostname: Option<String>, pub hostname: Option<String>,
pub host_name: Option<String>, pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>, pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>, pub port: Option<u16>,
pub activate_on_startup: Option<bool>, pub activate_on_startup: Option<bool>,
pub enter_hook: Option<String>,
} }
impl ConfigToml { impl ConfigToml {
@@ -50,7 +58,7 @@ struct CliArgs {
/// the frontend to use [cli | gtk] /// the frontend to use [cli | gtk]
#[arg(short, long)] #[arg(short, long)]
frontend: Option<String>, frontend: Option<Frontend>,
/// non-default config file location /// non-default config file location
#[arg(short, long)] #[arg(short, long)]
@@ -67,16 +75,144 @@ struct CliArgs {
/// test input emulation /// test input emulation
#[arg(long)] #[arg(long)]
test_emulation: bool, test_emulation: bool,
/// capture backend override
#[arg(long)]
capture_backend: Option<CaptureBackend>,
/// emulation backend override
#[arg(long)]
emulation_backend: Option<EmulationBackend>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend {
#[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 CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
CaptureBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
CaptureBackend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
CaptureBackend::MacOs => write!(f, "MacOS"),
CaptureBackend::Dummy => write!(f, "dummy"),
}
}
}
impl From<CaptureBackend> for input_capture::Backend {
fn from(backend: CaptureBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
CaptureBackend::X11 => Self::X11,
#[cfg(windows)]
CaptureBackend::Windows => Self::Windows,
#[cfg(target_os = "macos")]
CaptureBackend::MacOs => Self::MacOs,
CaptureBackend::Dummy => Self::Dummy,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum EmulationBackend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
MacOs,
Dummy,
}
impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
EmulationBackend::Libei => Self::Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
EmulationBackend::Xdp => Self::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
EmulationBackend::X11 => Self::X11,
#[cfg(windows)]
EmulationBackend::Windows => Self::Windows,
#[cfg(target_os = "macos")]
EmulationBackend::MacOs => Self::MacOs,
EmulationBackend::Dummy => Self::Dummy,
}
}
}
impl Display for EmulationBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
EmulationBackend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
EmulationBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
EmulationBackend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
EmulationBackend::MacOs => write!(f, "macos"),
EmulationBackend::Dummy => write!(f, "dummy"),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
pub enum Frontend { pub enum Frontend {
Gtk, Gtk,
Cli, Cli,
} }
impl Default for Frontend {
fn default() -> Self {
if cfg!(feature = "gtk") {
Self::Gtk
} else {
Self::Cli
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub frontend: Frontend, pub frontend: Frontend,
pub port: u16, pub port: u16,
pub clients: Vec<(TomlClient, Position)>, pub clients: Vec<(TomlClient, Position)>,
@@ -92,6 +228,7 @@ pub struct ConfigClient {
pub port: u16, pub port: u16,
pub pos: Position, pub pos: Position,
pub active: bool, pub active: bool,
pub enter_hook: Option<String>,
} }
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
@@ -127,33 +264,14 @@ impl Config {
Ok(c) => Some(c), Ok(c) => Some(c),
}; };
let frontend = match args.frontend { let frontend_arg = args.frontend;
None => match &config_toml { let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
Some(c) => c.frontend.clone(), let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
None => None,
},
frontend => frontend,
};
let frontend = match frontend { let port = args
#[cfg(feature = "gtk")] .port
None => Frontend::Gtk, .or(config_toml.as_ref().and_then(|c| c.port))
#[cfg(not(feature = "gtk"))] .unwrap_or(DEFAULT_PORT);
None => Frontend::Cli,
Some(s) => match s.as_str() {
"cli" => Frontend::Cli,
"gtk" => Frontend::Gtk,
_ => Frontend::Cli,
},
};
let port = match args.port {
Some(port) => port,
None => match &config_toml {
Some(c) => c.port.unwrap_or(DEFAULT_PORT),
None => DEFAULT_PORT,
},
};
log::debug!("{config_toml:?}"); log::debug!("{config_toml:?}");
let release_bind = config_toml let release_bind = config_toml
@@ -161,6 +279,14 @@ impl Config {
.and_then(|c| c.release_bind.clone()) .and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())); .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let capture_backend = args
.capture_backend
.or(config_toml.as_ref().and_then(|c| c.capture_backend));
let emulation_backend = args
.emulation_backend
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
let mut clients: Vec<(TomlClient, Position)> = vec![]; let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml { if let Some(config_toml) = config_toml {
@@ -183,6 +309,8 @@ impl Config {
let test_emulation = args.test_emulation; let test_emulation = args.test_emulation;
Ok(Config { Ok(Config {
capture_backend,
emulation_backend,
daemon, daemon,
frontend, frontend,
clients, clients,
@@ -208,12 +336,14 @@ impl Config {
None => c.host_name.clone(), None => c.host_name.clone(),
}; };
let active = c.activate_on_startup.unwrap_or(false); let active = c.activate_on_startup.unwrap_or(false);
let enter_hook = c.enter_hook.clone();
ConfigClient { ConfigClient {
ips, ips,
hostname, hostname,
port, port,
pos: *pos, pos: *pos,
active, active,
enter_hook,
} }
}) })
.collect() .collect()

View File

@@ -1,98 +0,0 @@
use async_trait::async_trait;
use std::future;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
use anyhow::Result;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
pub mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
/// fallback input emulation (logs events)
pub mod dummy;
#[async_trait]
pub trait InputEmulation: Send {
async fn consume(&mut self, event: Event, client_handle: ClientHandle);
async fn notify(&mut self, client_event: ClientEvent);
/// this function is waited on continuously and can be used to handle events
async fn dispatch(&mut self) -> Result<()> {
let _: () = future::pending().await;
Ok(())
}
async fn destroy(&mut self);
}
pub async fn create() -> Box<dyn InputEmulation> {
#[cfg(windows)]
match windows::WindowsEmulation::new() {
Ok(c) => return Box::new(c),
Err(e) => log::warn!("windows input emulation unavailable: {e}"),
}
#[cfg(target_os = "macos")]
match macos::MacOSEmulation::new() {
Ok(c) => {
log::info!("using macos input emulation");
return Box::new(c);
}
Err(e) => log::error!("macos input emulatino not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match wlroots::WlrootsEmulation::new() {
Ok(c) => {
log::info!("using wlroots input emulation");
return Box::new(c);
}
Err(e) => log::info!("wayland backend not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match libei::LibeiEmulation::new().await {
Ok(c) => {
log::info!("using libei input emulation");
return Box::new(c);
}
Err(e) => log::info!("libei not available: {e}"),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
match xdg_desktop_portal::DesktopPortalEmulation::new().await {
Ok(c) => {
log::info!("using xdg-remote-desktop-portal input emulation");
return Box::new(c);
}
Err(e) => log::info!("remote desktop portal not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match x11::X11Emulation::new() {
Ok(c) => {
log::info!("using x11 input emulation");
return Box::new(c);
}
Err(e) => log::info!("x11 input emulation not available: {e}"),
}
log::error!("falling back to dummy input emulation");
Box::new(dummy::DummyEmulation::new())
}

View File

@@ -1,26 +0,0 @@
use crate::{
client::{ClientEvent, ClientHandle},
emulate::InputEmulation,
event::Event,
};
use async_trait::async_trait;
#[derive(Default)]
pub struct DummyEmulation;
impl DummyEmulation {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl InputEmulation for DummyEmulation {
async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
log::info!("received event: ({client_handle}) {event}");
}
async fn notify(&mut self, client_event: ClientEvent) {
log::info!("{client_event:?}");
}
async fn destroy(&mut self) {}
}

View File

@@ -1,399 +0,0 @@
use std::{
collections::HashMap,
os::{fd::OwnedFd, unix::net::UnixStream},
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::{anyhow, Result};
use ashpd::{
desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
ResponseError,
},
WindowIdentifier,
};
use async_trait::async_trait;
use futures::StreamExt;
use reis::{
ei::{self, button::ButtonState, handshake::ContextType, keyboard::KeyState},
tokio::EiEventStream,
PendingRequestResult,
};
use crate::{
client::{ClientEvent, ClientHandle},
emulate::InputEmulation,
event::Event,
};
pub struct LibeiEmulation {
handshake: bool,
context: ei::Context,
events: EiEventStream,
pointer: Option<(ei::Device, ei::Pointer)>,
has_pointer: bool,
scroll: Option<(ei::Device, ei::Scroll)>,
has_scroll: bool,
button: Option<(ei::Device, ei::Button)>,
has_button: bool,
keyboard: Option<(ei::Device, ei::Keyboard)>,
has_keyboard: bool,
capabilities: HashMap<String, u64>,
capability_mask: u64,
sequence: u32,
serial: u32,
}
async fn get_ei_fd() -> Result<OwnedFd, ashpd::Error> {
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
let (session, _) = loop {
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
log::info!("requesting permission for input emulation");
match proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()
{
Ok(d) => break (session, d),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
log::warn!("request cancelled!");
continue;
}
e => e?,
};
};
proxy.connect_to_eis(&session).await
}
impl LibeiEmulation {
pub async fn new() -> Result<Self> {
// fd is owned by the message, so we need to dup it
let eifd = get_ei_fd().await?;
let stream = UnixStream::from(eifd);
// let stream = UnixStream::connect("/run/user/1000/eis-0")?;
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
context.flush()?;
let events = EiEventStream::new(context.clone())?;
Ok(Self {
handshake: false,
context,
events,
pointer: None,
button: None,
scroll: None,
keyboard: None,
has_pointer: false,
has_button: false,
has_scroll: false,
has_keyboard: false,
capabilities: HashMap::new(),
capability_mask: 0,
sequence: 0,
serial: 0,
})
}
}
#[async_trait]
impl InputEmulation for LibeiEmulation {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
match event {
Event::Pointer(p) => match p {
crate::event::PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if !self.has_pointer {
return;
}
if let Some((d, p)) = self.pointer.as_mut() {
p.motion_relative(relative_x as f32, relative_y as f32);
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Button {
time: _,
button,
state,
} => {
if !self.has_button {
return;
}
if let Some((d, b)) = self.button.as_mut() {
b.button(
button,
match state {
0 => ButtonState::Released,
_ => ButtonState::Press,
},
);
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Axis {
time: _,
axis,
value,
} => {
if !self.has_scroll {
return;
}
if let Some((d, s)) = self.scroll.as_mut() {
match axis {
0 => s.scroll(0., value as f32),
_ => s.scroll(value as f32, 0.),
}
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::AxisDiscrete120 { axis, value } => {
if !self.has_scroll {
return;
}
if let Some((d, s)) = self.scroll.as_mut() {
match axis {
0 => s.scroll_discrete(0, value),
_ => s.scroll_discrete(value, 0),
}
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Frame {} => {}
},
Event::Keyboard(k) => match k {
crate::event::KeyboardEvent::Key {
time: _,
key,
state,
} => {
if !self.has_keyboard {
return;
}
if let Some((d, k)) = &mut self.keyboard {
k.key(
key,
match state {
0 => KeyState::Released,
_ => KeyState::Press,
},
);
d.frame(self.serial, now);
}
}
crate::event::KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
self.context.flush().unwrap();
}
async fn dispatch(&mut self) -> Result<()> {
let event = match self.events.next().await {
Some(e) => e?,
None => return Err(anyhow!("libei connection lost")),
};
let event = match event {
PendingRequestResult::Request(result) => result,
PendingRequestResult::ParseError(e) => {
return Err(anyhow!("libei protocol violation: {e}"))
}
PendingRequestResult::InvalidObject(e) => return Err(anyhow!("invalid object {e}")),
};
match event {
ei::Event::Handshake(handshake, request) => match request {
ei::handshake::Event::HandshakeVersion { version } => {
if self.handshake {
return Ok(());
}
log::info!("libei version {}", version);
// sender means we are sending events _to_ the eis server
handshake.handshake_version(version); // FIXME
handshake.context_type(ContextType::Sender);
handshake.name("ei-demo-client");
handshake.interface_version("ei_connection", 1);
handshake.interface_version("ei_callback", 1);
handshake.interface_version("ei_pingpong", 1);
handshake.interface_version("ei_seat", 1);
handshake.interface_version("ei_device", 2);
handshake.interface_version("ei_pointer", 1);
handshake.interface_version("ei_pointer_absolute", 1);
handshake.interface_version("ei_scroll", 1);
handshake.interface_version("ei_button", 1);
handshake.interface_version("ei_keyboard", 1);
handshake.interface_version("ei_touchscreen", 1);
handshake.finish();
self.handshake = true;
}
ei::handshake::Event::InterfaceVersion { name, version } => {
log::debug!("handshake: Interface {name} @ {version}");
}
ei::handshake::Event::Connection { serial, connection } => {
connection.sync(1);
self.serial = serial;
}
_ => unreachable!(),
},
ei::Event::Connection(_connection, request) => match request {
ei::connection::Event::Seat { seat } => {
log::debug!("connected to seat: {seat:?}");
}
ei::connection::Event::Ping { ping } => {
ping.done(0);
}
ei::connection::Event::Disconnected {
last_serial: _,
reason,
explanation,
} => {
log::debug!("ei - disconnected: reason: {reason:?}: {explanation}")
}
ei::connection::Event::InvalidObject {
last_serial,
invalid_id,
} => {
return Err(anyhow!(
"invalid object: id: {invalid_id}, serial: {last_serial}"
));
}
_ => unreachable!(),
},
ei::Event::Device(device, request) => match request {
ei::device::Event::Destroyed { serial } => {
log::debug!("device destroyed: {device:?} - serial: {serial}")
}
ei::device::Event::Name { name } => {
log::debug!("device name: {name}")
}
ei::device::Event::DeviceType { device_type } => {
log::debug!("device type: {device_type:?}")
}
ei::device::Event::Dimensions { width, height } => {
log::debug!("device dimensions: {width}x{height}")
}
ei::device::Event::Region {
offset_x,
offset_y,
width,
hight,
scale,
} => log::debug!(
"device region: {width}x{hight} @ ({offset_x},{offset_y}), scale: {scale}"
),
ei::device::Event::Interface { object } => {
log::debug!("device interface: {object:?}");
if object.interface().eq("ei_pointer") {
log::debug!("GOT POINTER DEVICE");
self.pointer.replace((device, object.downcast().unwrap()));
} else if object.interface().eq("ei_button") {
log::debug!("GOT BUTTON DEVICE");
self.button.replace((device, object.downcast().unwrap()));
} else if object.interface().eq("ei_scroll") {
log::debug!("GOT SCROLL DEVICE");
self.scroll.replace((device, object.downcast().unwrap()));
} else if object.interface().eq("ei_keyboard") {
log::debug!("GOT KEYBOARD DEVICE");
self.keyboard.replace((device, object.downcast().unwrap()));
}
}
ei::device::Event::Done => {
log::debug!("device: done {device:?}");
}
ei::device::Event::Resumed { serial } => {
self.serial = serial;
device.start_emulating(serial, self.sequence);
self.sequence += 1;
log::debug!("resumed: {device:?}");
if let Some((d, _)) = &mut self.pointer {
if d == &device {
log::debug!("pointer resumed {serial}");
self.has_pointer = true;
}
}
if let Some((d, _)) = &mut self.button {
if d == &device {
log::debug!("button resumed {serial}");
self.has_button = true;
}
}
if let Some((d, _)) = &mut self.scroll {
if d == &device {
log::debug!("scroll resumed {serial}");
self.has_scroll = true;
}
}
if let Some((d, _)) = &mut self.keyboard {
if d == &device {
log::debug!("keyboard resumed {serial}");
self.has_keyboard = true;
}
}
}
ei::device::Event::Paused { serial } => {
self.has_pointer = false;
self.has_button = false;
self.serial = serial;
}
ei::device::Event::StartEmulating { serial, sequence } => {
log::debug!("start emulating {serial}, {sequence}")
}
ei::device::Event::StopEmulating { serial } => {
log::debug!("stop emulating {serial}")
}
ei::device::Event::Frame { serial, timestamp } => {
log::debug!("frame: {serial}, {timestamp}");
}
ei::device::Event::RegionMappingId { mapping_id } => {
log::debug!("RegionMappingId {mapping_id}")
}
e => log::debug!("invalid event: {e:?}"),
},
ei::Event::Seat(seat, request) => match request {
ei::seat::Event::Destroyed { serial } => {
self.serial = serial;
log::debug!("seat destroyed: {seat:?}");
}
ei::seat::Event::Name { name } => {
log::debug!("seat name: {name}");
}
ei::seat::Event::Capability { mask, interface } => {
log::debug!("seat capabilities: {mask}, interface: {interface:?}");
self.capabilities.insert(interface, mask);
self.capability_mask |= mask;
}
ei::seat::Event::Done => {
log::debug!("seat done");
log::debug!("binding capabilities: {}", self.capability_mask);
seat.bind(self.capability_mask);
}
ei::seat::Event::Device { device } => {
log::debug!("seat: new device - {device:?}");
}
_ => todo!(),
},
e => log::debug!("unhandled event: {e:?}"),
}
self.context.flush()?;
Ok(())
}
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
}

View File

@@ -1,7 +1,6 @@
use crate::client::{ClientEvent, Position}; use crate::config::Config;
use crate::emulate;
use crate::event::{Event, PointerEvent};
use anyhow::Result; use anyhow::Result;
use input_event::{Event, PointerEvent};
use std::f64::consts::PI; use std::f64::consts::PI;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::task::LocalSet; use tokio::task::LocalSet;
@@ -13,36 +12,34 @@ pub fn run() -> Result<()> {
.enable_time() .enable_time()
.build()?; .build()?;
runtime.block_on(LocalSet::new().run_until(input_emulation_test())) let config = Config::new()?;
runtime.block_on(LocalSet::new().run_until(input_emulation_test(config)))
} }
const FREQUENCY_HZ: f64 = 1.0; const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0; const RADIUS: f64 = 100.0;
async fn input_emulation_test() -> Result<()> { async fn input_emulation_test(config: Config) -> Result<()> {
let mut emulation = emulate::create().await; let backend = config.emulation_backend.map(|b| b.into());
emulation let mut emulation = input_emulation::create(backend).await?;
.notify(ClientEvent::Create(0, Position::Left)) emulation.create(0).await;
.await;
let start = Instant::now(); let start = Instant::now();
let mut offset = (0, 0); let mut offset = (0, 0);
loop { loop {
tokio::select! { tokio::time::sleep(Duration::from_millis(1)).await;
_ = emulation.dispatch() => {} let elapsed = start.elapsed();
_ = tokio::time::sleep(Duration::from_millis(1)) => { let elapsed_sec_f64 = elapsed.as_secs_f64();
let elapsed = start.elapsed(); let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
let elapsed_sec_f64 = elapsed.as_secs_f64(); let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64; let new_offset_f = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
let radians = second_fraction * 2. * PI * FREQUENCY_HZ; let new_offset = (new_offset_f.0 as i32, new_offset_f.1 as i32);
let new_offset_f = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS); if new_offset != offset {
let new_offset = (new_offset_f.0 as i32, new_offset_f.1 as i32); let relative_motion = (new_offset.0 - offset.0, new_offset.1 - offset.1);
if new_offset != offset { offset = new_offset;
let relative_motion = (new_offset.0 - offset.0, new_offset.1 - offset.1); let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
offset = new_offset; let event = Event::Pointer(PointerEvent::Motion { time: 0, dx, dy });
let (relative_x, relative_y) = (relative_motion.0 as f64, relative_motion.1 as f64); emulation.consume(event, 0).await?;
emulation.consume(Event::Pointer(PointerEvent::Motion {time: 0, relative_x, relative_y }), 0).await;
}
}
} }
} }
} }

View File

@@ -1,586 +0,0 @@
use crate::scancode;
use anyhow::{anyhow, Result};
use std::{
error::Error,
fmt::{self, Display},
};
// FIXME
pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111;
pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PointerEvent {
Motion {
time: u32,
relative_x: f64,
relative_y: f64,
},
Button {
time: u32,
button: u32,
state: u32,
},
Axis {
time: u32,
axis: u8,
value: f64,
},
AxisDiscrete120 {
axis: u8,
value: i32,
},
Frame {},
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum KeyboardEvent {
Key {
time: u32,
key: u32,
state: u8,
},
Modifiers {
mods_depressed: u32,
mods_latched: u32,
mods_locked: u32,
group: u32,
},
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Event {
/// pointer event (motion / button / axis)
Pointer(PointerEvent),
/// keyboard events (key / modifiers)
Keyboard(KeyboardEvent),
/// enter event: request to enter a client.
/// The client must release the pointer if it is grabbed
/// and reply with a leave event, as soon as its ready to
/// receive events
Enter(),
/// leave event: this client is now ready to receive events and will
/// not send any events after until it sends an enter event
Leave(),
/// ping a client, to see if it is still alive. A client that does
/// not respond with a pong event will be assumed to be offline.
Ping(),
/// response to a ping event: this event signals that a client
/// is still alive but must otherwise be ignored
Pong(),
/// explicit disconnect request. The client will no longer
/// send events until the next Enter event. All of its keys should be released.
Disconnect(),
}
impl Display for PointerEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => write!(f, "motion({relative_x},{relative_y})"),
PointerEvent::Button {
time: _,
button,
state,
} => {
let str = match *button {
BTN_LEFT => Some("left"),
BTN_RIGHT => Some("right"),
BTN_MIDDLE => Some("middle"),
BTN_FORWARD => Some("forward"),
BTN_BACK => Some("back"),
_ => None,
};
if let Some(button) = str {
write!(f, "button({button}, {state})")
} else {
write!(f, "button({button}, {state}")
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => write!(f, "scroll({axis}, {value})"),
PointerEvent::AxisDiscrete120 { axis, value } => {
write!(f, "scroll-120 ({axis}, {value})")
}
PointerEvent::Frame {} => write!(f, "frame()"),
}
}
}
impl Display for KeyboardEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let scan = scancode::Linux::try_from(*key);
if let Ok(scan) = scan {
write!(f, "key({scan:?}, {state})")
} else {
write!(f, "key({key}, {state})")
}
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => write!(
f,
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
),
}
}
}
impl Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k),
Event::Enter() => write!(f, "enter"),
Event::Leave() => write!(f, "leave"),
Event::Ping() => write!(f, "ping"),
Event::Pong() => write!(f, "pong"),
Event::Disconnect() => write!(f, "disconnect"),
}
}
}
impl Event {
fn event_type(&self) -> EventType {
match self {
Self::Pointer(_) => EventType::Pointer,
Self::Keyboard(_) => EventType::Keyboard,
Self::Enter() => EventType::Enter,
Self::Leave() => EventType::Leave,
Self::Ping() => EventType::Ping,
Self::Pong() => EventType::Pong,
Self::Disconnect() => EventType::Disconnect,
}
}
}
impl PointerEvent {
fn event_type(&self) -> PointerEventType {
match self {
Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::Axis,
Self::AxisDiscrete120 { .. } => PointerEventType::AxisDiscrete120,
Self::Frame { .. } => PointerEventType::Frame,
}
}
}
impl KeyboardEvent {
fn event_type(&self) -> KeyboardEventType {
match self {
KeyboardEvent::Key { .. } => KeyboardEventType::Key,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
}
}
}
enum PointerEventType {
Motion,
Button,
Axis,
AxisDiscrete120,
Frame,
}
enum KeyboardEventType {
Key,
Modifiers,
}
enum EventType {
Pointer,
Keyboard,
Enter,
Leave,
Ping,
Pong,
Disconnect,
}
impl TryFrom<u8> for PointerEventType {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self> {
match value {
x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::AxisDiscrete120 as u8 => Ok(Self::AxisDiscrete120),
x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid pointer event type {}", value),
})),
}
}
}
impl TryFrom<u8> for KeyboardEventType {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self> {
match value {
x if x == Self::Key as u8 => Ok(Self::Key),
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid keyboard event type {}", value),
})),
}
}
}
impl From<&Event> for Vec<u8> {
fn from(event: &Event) -> Self {
let event_id = vec![event.event_type() as u8];
let event_data = match event {
Event::Pointer(p) => p.into(),
Event::Keyboard(k) => k.into(),
Event::Enter() => vec![],
Event::Leave() => vec![],
Event::Ping() => vec![],
Event::Pong() => vec![],
Event::Disconnect() => vec![],
};
[event_id, event_data].concat()
}
}
#[derive(Debug)]
struct ProtocolError {
msg: String,
}
impl fmt::Display for ProtocolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Protocol violation: {}", self.msg)
}
}
impl Error for ProtocolError {}
impl TryFrom<Vec<u8>> for Event {
type Error = anyhow::Error;
fn try_from(value: Vec<u8>) -> Result<Self> {
let event_id = u8::from_be_bytes(value[..1].try_into()?);
match event_id {
i if i == (EventType::Pointer as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
i if i == (EventType::Disconnect as u8) => Ok(Event::Disconnect()),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid event_id {}", event_id),
})),
}
}
}
impl From<&PointerEvent> for Vec<u8> {
fn from(event: &PointerEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
PointerEvent::Motion {
time,
relative_x,
relative_y,
} => {
let time = time.to_be_bytes();
let relative_x = relative_x.to_be_bytes();
let relative_y = relative_y.to_be_bytes();
[&time[..], &relative_x[..], &relative_y[..]].concat()
}
PointerEvent::Button {
time,
button,
state,
} => {
let time = time.to_be_bytes();
let button = button.to_be_bytes();
let state = state.to_be_bytes();
[&time[..], &button[..], &state[..]].concat()
}
PointerEvent::Axis { time, axis, value } => {
let time = time.to_be_bytes();
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
[&time[..], &axis[..], &value[..]].concat()
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
[&axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => {
vec![]
}
};
[id, data].concat()
}
}
impl TryFrom<Vec<u8>> for PointerEvent {
type Error = anyhow::Error;
fn try_from(data: Vec<u8>) -> Result<Self> {
match data.get(1) {
Some(id) => {
let event_type = match id.to_owned().try_into() {
Ok(event_type) => event_type,
Err(e) => return Err(e),
};
match event_type {
PointerEventType::Motion => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
};
let relative_x = match data.get(6..14) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 8 Bytes at index 6".into(),
}))
}
};
let relative_y = match data.get(14..22) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 8 Bytes at index 14".into(),
}))
}
};
Ok(Self::Motion {
time,
relative_x,
relative_y,
})
}
PointerEventType::Button => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
};
let button = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
};
let state = match data.get(10..14) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 14".into(),
}))
}
};
Ok(Self::Button {
time,
button,
state,
})
}
PointerEventType::Axis => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
};
let axis = match data.get(6) {
Some(d) => *d,
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Byte at index 6".into(),
}));
}
};
let value = match data.get(7..15) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 8 Bytes at index 7".into(),
}));
}
};
Ok(Self::Axis { time, axis, value })
}
PointerEventType::AxisDiscrete120 => {
let axis = match data.get(2) {
Some(d) => *d,
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Byte at index 2".into(),
}));
}
};
let value = match data.get(3..7) {
Some(d) => i32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 3".into(),
}));
}
};
Ok(Self::AxisDiscrete120 { axis, value })
}
PointerEventType::Frame => Ok(Self::Frame {}),
}
}
None => Err(anyhow!(ProtocolError {
msg: "Expected an element at index 0".into(),
})),
}
}
}
impl From<&KeyboardEvent> for Vec<u8> {
fn from(event: &KeyboardEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
KeyboardEvent::Key { time, key, state } => {
let time = time.to_be_bytes();
let key = key.to_be_bytes();
let state = state.to_be_bytes();
[&time[..], &key[..], &state[..]].concat()
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => {
let mods_depressed = mods_depressed.to_be_bytes();
let mods_latched = mods_latched.to_be_bytes();
let mods_locked = mods_locked.to_be_bytes();
let group = group.to_be_bytes();
[
&mods_depressed[..],
&mods_latched[..],
&mods_locked[..],
&group[..],
]
.concat()
}
};
[id, data].concat()
}
}
impl TryFrom<Vec<u8>> for KeyboardEvent {
type Error = anyhow::Error;
fn try_from(data: Vec<u8>) -> Result<Self> {
match data.get(1) {
Some(id) => {
let event_type = match id.to_owned().try_into() {
Ok(event_type) => event_type,
Err(e) => return Err(e),
};
match event_type {
KeyboardEventType::Key => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 6".into(),
}))
}
};
let key = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
};
let state = match data.get(10) {
Some(d) => *d,
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Bytes at index 14".into(),
}))
}
};
Ok(KeyboardEvent::Key { time, key, state })
}
KeyboardEventType::Modifiers => {
let mods_depressed = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 6".into(),
}))
}
};
let mods_latched = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
};
let mods_locked = match data.get(10..14) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 14".into(),
}))
}
};
let group = match data.get(14..18) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 18".into(),
}))
}
};
Ok(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
})
}
}
}
None => Err(anyhow!(ProtocolError {
msg: "Expected an element at index 0".into(),
})),
}
}
}

View File

@@ -11,6 +11,7 @@ use std::{
use crate::frontend::{gtk::window::Window, FrontendRequest}; use crate::frontend::{gtk::window::Window, FrontendRequest};
use adw::Application; use adw::Application;
use endi::{Endian, ReadBytes};
use gtk::{ use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme, gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
}; };
@@ -85,16 +86,14 @@ fn build_ui(app: &Application) {
gio::spawn_blocking(move || { gio::spawn_blocking(move || {
match loop { match loop {
// read length // read length
let mut len = [0u8; 8]; let len = match rx.read_u64(Endian::Big) {
match rx.read_exact(&mut len) { Ok(l) => l,
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e), Err(e) => break Err(e),
}; };
let len = usize::from_be_bytes(len);
// read payload // read payload
let mut buf = vec![0u8; len]; let mut buf = vec![0u8; len as usize];
match rx.read_exact(&mut buf) { match rx.read_exact(&mut buf) {
Ok(_) => (), Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),

View File

@@ -10,6 +10,7 @@ use std::net::TcpStream;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use endi::{Endian, WriteBytes};
use glib::{clone, Object}; use glib::{clone, Object};
use gtk::{ use gtk::{
gio, gio,
@@ -265,8 +266,7 @@ impl Window {
let mut stream = self.imp().stream.borrow_mut(); let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap(); let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes(); let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes(); if let Err(e) = stream.write_u64(Endian::Big, bytes.len() as u64) {
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}"); log::error!("error sending message: {e}");
}; };
if let Err(e) = stream.write(bytes) { if let Err(e) = stream.write(bytes) {

View File

@@ -1,13 +1,8 @@
pub mod client; pub mod client;
pub mod config; pub mod config;
pub mod dns; pub mod dns;
pub mod event;
pub mod server; pub mod server;
pub mod capture;
pub mod emulate;
pub mod capture_test; pub mod capture_test;
pub mod emulation_test; pub mod emulation_test;
pub mod frontend; pub mod frontend;
pub mod scancode;

View File

@@ -68,10 +68,12 @@ fn run_service(config: &Config) -> Result<()> {
// run async event loop // run async event loop
runtime.block_on(LocalSet::new().run_until(async { runtime.block_on(LocalSet::new().run_until(async {
// run main loop // run main loop
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse"); log::info!("Press {:?} to release the mouse", config.release_bind);
let server = Server::new(config); let server = Server::new(config);
server.run().await?; server
.run(config.capture_backend, config.emulation_backend)
.await?;
log::debug!("service exiting"); log::debug!("service exiting");
anyhow::Ok(()) anyhow::Ok(())

View File

@@ -8,7 +8,7 @@ use tokio::signal;
use crate::{ use crate::{
client::{ClientConfig, ClientHandle, ClientManager, ClientState}, client::{ClientConfig, ClientHandle, ClientManager, ClientState},
config::Config, config::{CaptureBackend, Config, EmulationBackend},
dns, dns,
frontend::{FrontendListener, FrontendRequest}, frontend::{FrontendListener, FrontendRequest},
server::capture_task::CaptureEvent, server::capture_task::CaptureEvent,
@@ -40,7 +40,7 @@ pub struct Server {
client_manager: Rc<RefCell<ClientManager>>, client_manager: Rc<RefCell<ClientManager>>,
port: Rc<Cell<u16>>, port: Rc<Cell<u16>>,
state: Rc<Cell<State>>, state: Rc<Cell<State>>,
release_bind: Vec<crate::scancode::Linux>, release_bind: Vec<input_event::scancode::Linux>,
} }
impl Server { impl Server {
@@ -55,6 +55,7 @@ impl Server {
fix_ips: config_client.ips.into_iter().collect(), fix_ips: config_client.ips.into_iter().collect(),
port: config_client.port, port: config_client.port,
pos: config_client.pos, pos: config_client.pos,
cmd: config_client.enter_hook,
}; };
let state = ClientState { let state = ClientState {
active: config_client.active, active: config_client.active,
@@ -76,7 +77,11 @@ impl Server {
} }
} }
pub async fn run(&self) -> anyhow::Result<()> { pub async fn run(
&self,
capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>,
) -> anyhow::Result<()> {
// create frontend communication adapter // create frontend communication adapter
let frontend = match FrontendListener::new().await { let frontend = match FrontendListener::new().await {
Some(f) => f?, Some(f) => f?,
@@ -96,14 +101,16 @@ impl Server {
// input capture // input capture
let (mut capture_task, capture_channel) = capture_task::new( let (mut capture_task, capture_channel) = capture_task::new(
capture_backend,
self.clone(), self.clone(),
sender_tx.clone(), sender_tx.clone(),
timer_tx.clone(), timer_tx.clone(),
self.release_bind.clone(), self.release_bind.clone(),
); )?;
// input emulation // input emulation
let (mut emulation_task, emulate_channel) = emulation_task::new( let (mut emulation_task, emulate_channel) = emulation_task::new(
emulation_backend,
self.clone(), self.clone(),
receiver_rx, receiver_rx,
sender_tx.clone(), sender_tx.clone(),

View File

@@ -2,15 +2,13 @@ use anyhow::{anyhow, Result};
use futures::StreamExt; use futures::StreamExt;
use std::{collections::HashSet, net::SocketAddr}; use std::{collections::HashSet, net::SocketAddr};
use tokio::{sync::mpsc::Sender, task::JoinHandle}; use tokio::{process::Command, sync::mpsc::Sender, task::JoinHandle};
use crate::{ use input_capture::{self, error::CaptureCreationError, CaptureHandle, InputCapture, Position};
capture::{self, InputCapture},
client::{ClientEvent, ClientHandle}, use input_event::{scancode, Event, KeyboardEvent};
event::{Event, KeyboardEvent},
scancode, use crate::{client::ClientHandle, config::CaptureBackend, server::State};
server::State,
};
use super::Server; use super::Server;
@@ -18,21 +16,25 @@ use super::Server;
pub enum CaptureEvent { pub enum CaptureEvent {
/// capture must release the mouse /// capture must release the mouse
Release, Release,
/// capture is notified of a change in client states /// add a capture client
ClientEvent(ClientEvent), Create(CaptureHandle, Position),
/// destory a capture client
Destroy(CaptureHandle),
/// termination signal /// termination signal
Terminate, Terminate,
} }
pub fn new( pub fn new(
backend: Option<CaptureBackend>,
server: Server, server: Server,
sender_tx: Sender<(Event, SocketAddr)>, sender_tx: Sender<(Event, SocketAddr)>,
timer_tx: Sender<()>, timer_tx: Sender<()>,
release_bind: Vec<scancode::Linux>, release_bind: Vec<scancode::Linux>,
) -> (JoinHandle<Result<()>>, Sender<CaptureEvent>) { ) -> Result<(JoinHandle<Result<()>>, Sender<CaptureEvent>), CaptureCreationError> {
let (tx, mut rx) = tokio::sync::mpsc::channel(32); let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let backend = backend.map(|b| b.into());
let task = tokio::task::spawn_local(async move { let task = tokio::task::spawn_local(async move {
let mut capture = capture::create().await; let mut capture = input_capture::create(backend).await?;
let mut pressed_keys = HashSet::new(); let mut pressed_keys = HashSet::new();
loop { loop {
tokio::select! { tokio::select! {
@@ -50,9 +52,9 @@ pub fn new(
CaptureEvent::Release => { CaptureEvent::Release => {
capture.release()?; capture.release()?;
server.state.replace(State::Receiving); server.state.replace(State::Receiving);
} }
CaptureEvent::ClientEvent(e) => capture.notify(e)?, CaptureEvent::Create(h, p) => capture.create(h, p)?,
CaptureEvent::Destroy(h) => capture.destroy(h)?,
CaptureEvent::Terminate => break, CaptureEvent::Terminate => break,
}, },
None => break, None => break,
@@ -62,7 +64,7 @@ pub fn new(
} }
anyhow::Ok(()) anyhow::Ok(())
}); });
(task, tx) Ok((task, tx))
} }
fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) { fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) {
@@ -80,7 +82,7 @@ async fn handle_capture_event(
capture: &mut Box<dyn InputCapture>, capture: &mut Box<dyn InputCapture>,
sender_tx: &Sender<(Event, SocketAddr)>, sender_tx: &Sender<(Event, SocketAddr)>,
timer_tx: &Sender<()>, timer_tx: &Sender<()>,
event: (ClientHandle, Event), event: (CaptureHandle, Event),
pressed_keys: &mut HashSet<scancode::Linux>, pressed_keys: &mut HashSet<scancode::Linux>,
release_bind: &[scancode::Linux], release_bind: &[scancode::Linux],
) -> Result<()> { ) -> Result<()> {
@@ -140,6 +142,9 @@ async fn handle_capture_event(
if start_timer { if start_timer {
let _ = timer_tx.try_send(()); let _ = timer_tx.try_send(());
} }
if enter {
spawn_hook_command(server, handle);
}
if let Some(addr) = addr { if let Some(addr) = addr {
if enter { if enter {
let _ = sender_tx.send((Event::Enter(), addr)).await; let _ = sender_tx.send((Event::Enter(), addr)).await;
@@ -148,3 +153,34 @@ async fn handle_capture_event(
} }
Ok(()) Ok(())
} }
fn spawn_hook_command(server: &Server, handle: ClientHandle) {
let Some(cmd) = server
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.cmd.clone())
else {
return;
};
tokio::task::spawn_local(async move {
log::info!("spawning command!");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute cmd: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
}
});
}

View File

@@ -1,25 +1,31 @@
use anyhow::{anyhow, Result};
use std::net::SocketAddr; use std::net::SocketAddr;
use thiserror::Error;
use tokio::{ use tokio::{
sync::mpsc::{Receiver, Sender}, sync::mpsc::{Receiver, Sender},
task::JoinHandle, task::JoinHandle,
}; };
use crate::{ use crate::{
client::{ClientEvent, ClientHandle}, client::{ClientHandle, ClientManager},
emulate::{self, InputEmulation}, config::EmulationBackend,
event::{Event, KeyboardEvent},
scancode,
server::State, server::State,
}; };
use input_emulation::{
self,
error::{EmulationCreationError, EmulationError},
EmulationHandle, InputEmulation,
};
use input_event::{Event, KeyboardEvent};
use super::{CaptureEvent, Server}; use super::{network_task::NetworkError, CaptureEvent, Server};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum EmulationEvent { pub enum EmulationEvent {
/// input emulation is notified of a change in client states /// create a new client
ClientEvent(ClientEvent), Create(EmulationHandle),
/// destroy a client
Destroy(EmulationHandle),
/// input emulation must release keys for client /// input emulation must release keys for client
ReleaseKeys(ClientHandle), ReleaseKeys(ClientHandle),
/// termination signal /// termination signal
@@ -27,55 +33,75 @@ pub enum EmulationEvent {
} }
pub fn new( pub fn new(
backend: Option<EmulationBackend>,
server: Server, server: Server,
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>, udp_rx: Receiver<Result<(Event, SocketAddr), NetworkError>>,
sender_tx: Sender<(Event, SocketAddr)>, sender_tx: Sender<(Event, SocketAddr)>,
capture_tx: Sender<CaptureEvent>, capture_tx: Sender<CaptureEvent>,
timer_tx: Sender<()>, timer_tx: Sender<()>,
) -> (JoinHandle<Result<()>>, Sender<EmulationEvent>) { ) -> (
let (tx, mut rx) = tokio::sync::mpsc::channel(32); JoinHandle<Result<(), LanMouseEmulationError>>,
let emulate_task = tokio::task::spawn_local(async move { Sender<EmulationEvent>,
let mut emulate = emulate::create().await; ) {
let mut last_ignored = None; let (tx, rx) = tokio::sync::mpsc::channel(32);
let emulation_task =
emulation_task(backend, rx, server, udp_rx, sender_tx, capture_tx, timer_tx);
let emulate_task = tokio::task::spawn_local(emulation_task);
(emulate_task, tx)
}
loop { #[derive(Debug, Error)]
tokio::select! { pub enum LanMouseEmulationError {
udp_event = udp_rx.recv() => { #[error("error creating input-emulation: `{0}`")]
let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??; Create(#[from] EmulationCreationError),
handle_udp_rx(&server, &capture_tx, &mut emulate, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await; #[error("error emulating input: `{0}`")]
} Emulate(#[from] EmulationError),
emulate_event = rx.recv() => { }
match emulate_event {
Some(e) => match e { async fn emulation_task(
EmulationEvent::ClientEvent(e) => emulate.notify(e).await, backend: Option<EmulationBackend>,
EmulationEvent::ReleaseKeys(c) => release_keys(&server, &mut emulate, c).await, mut rx: Receiver<EmulationEvent>,
EmulationEvent::Terminate => break, server: Server,
}, mut udp_rx: Receiver<Result<(Event, SocketAddr), NetworkError>>,
None => break, sender_tx: Sender<(Event, SocketAddr)>,
capture_tx: Sender<CaptureEvent>,
timer_tx: Sender<()>,
) -> Result<(), LanMouseEmulationError> {
let backend = backend.map(|b| b.into());
let mut emulation = input_emulation::create(backend).await?;
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = udp_rx.recv() => {
let udp_event = match udp_event {
Some(Ok(e)) => e,
Some(Err(e)) => {
log::warn!("network error: {e}");
continue;
} }
} None => break,
res = emulate.dispatch() => { };
res?; handle_udp_rx(&server, &capture_tx, &mut emulation, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await?;
}
emulate_event = rx.recv() => {
match emulate_event {
Some(e) => match e {
EmulationEvent::Create(h) => emulation.create(h).await,
EmulationEvent::Destroy(h) => emulation.destroy(h).await,
EmulationEvent::ReleaseKeys(c) => release_keys(&server, &mut emulation, c).await?,
EmulationEvent::Terminate => break,
},
None => break,
} }
} }
} }
}
// release potentially still pressed keys // release potentially still pressed keys
let clients = server release_all_keys(&server, &mut emulation).await?;
.client_manager
.borrow()
.get_client_states()
.map(|(h, _)| h)
.collect::<Vec<_>>();
for client in clients {
release_keys(&server, &mut emulate, client).await;
}
// destroy emulator Ok(())
emulate.destroy().await;
anyhow::Ok(())
});
(emulate_task, tx)
} }
async fn handle_udp_rx( async fn handle_udp_rx(
@@ -86,48 +112,25 @@ async fn handle_udp_rx(
last_ignored: &mut Option<SocketAddr>, last_ignored: &mut Option<SocketAddr>,
event: (Event, SocketAddr), event: (Event, SocketAddr),
timer_tx: &Sender<()>, timer_tx: &Sender<()>,
) { ) -> Result<(), EmulationError> {
let (event, addr) = event; let (event, addr) = event;
// get handle for addr log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
let handle = match server.client_manager.borrow().get_client(addr) {
Some(a) => a, // get client handle for addr
None => { let Some(handle) =
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr { activate_client_if_exists(&mut server.client_manager.borrow_mut(), addr, last_ignored)
log::warn!("ignoring events from client {addr}"); else {
last_ignored.replace(addr); return Ok(());
}
return;
}
}; };
// next event can be logged as ignored again
last_ignored.take();
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some((_, s)) => s,
None => {
log::error!("unknown handle");
return;
}
};
// reset ttl for client and
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
}
match (event, addr) { match (event, addr) {
(Event::Pong(), _) => { /* ignore pong events */ } (Event::Pong(), _) => { /* ignore pong events */ }
(Event::Ping(), addr) => { (Event::Ping(), addr) => {
let _ = sender_tx.send((Event::Pong(), addr)).await; let _ = sender_tx.send((Event::Pong(), addr)).await;
} }
(Event::Disconnect(), _) => { (Event::Disconnect(), _) => {
release_keys(server, emulate, handle).await; release_keys(server, emulate, handle).await?;
} }
(event, addr) => { (event, addr) => {
// tell clients that we are ready to receive events // tell clients that we are ready to receive events
@@ -148,34 +151,26 @@ async fn handle_udp_rx(
} }
} }
State::Receiving => { State::Receiving => {
let mut ignore_event = false; let ignore_event =
if let Event::Keyboard(KeyboardEvent::Key { if let Event::Keyboard(KeyboardEvent::Key { key, state, .. }) = event {
time: _, let (ignore_event, restart_timer) = update_client_keys(
key, &mut server.client_manager.borrow_mut(),
state, handle,
}) = event key,
{ state,
let mut client_manager = server.client_manager.borrow_mut(); );
let client_state = if let Some((_, s)) = client_manager.get_mut(handle) { // restart timer if necessary
s if restart_timer {
let _ = timer_tx.try_send(());
}
ignore_event
} else { } else {
log::error!("unknown handle"); false
return;
}; };
if state == 0 {
// ignore release event if key not pressed
ignore_event = !client_state.pressed_keys.remove(&key);
} else {
// ignore press event if key not released
ignore_event = !client_state.pressed_keys.insert(key);
let _ = timer_tx.try_send(());
}
}
// ignore double press / release events to
// workaround buggy rdp backend. // workaround buggy rdp backend.
if !ignore_event { if !ignore_event {
// consume event // consume event
emulate.consume(event, handle).await; emulate.consume(event, handle).await?;
log::trace!("{event} => emulate"); log::trace!("{event} => emulate");
} }
} }
@@ -200,13 +195,30 @@ async fn handle_udp_rx(
} }
} }
} }
Ok(())
}
async fn release_all_keys(
server: &Server,
emulation: &mut Box<dyn InputEmulation>,
) -> Result<(), EmulationError> {
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|(h, _)| h)
.collect::<Vec<_>>();
for client in clients {
release_keys(server, emulation, client).await?;
}
Ok(())
} }
async fn release_keys( async fn release_keys(
server: &Server, server: &Server,
emulate: &mut Box<dyn InputEmulation>, emulate: &mut Box<dyn InputEmulation>,
client: ClientHandle, client: ClientHandle,
) { ) -> Result<(), EmulationError> {
let keys = server let keys = server
.client_manager .client_manager
.borrow_mut() .borrow_mut()
@@ -221,19 +233,65 @@ async fn release_keys(
key, key,
state: 0, state: 0,
}); });
emulate.consume(event, client).await; emulate.consume(event, client).await?;
if let Ok(key) = scancode::Linux::try_from(key) { if let Ok(key) = input_event::scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}"); log::warn!("releasing stuck key: {key:?}");
} }
} }
let modifiers_event = KeyboardEvent::Modifiers { let event = Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed: 0, mods_depressed: 0,
mods_latched: 0, mods_latched: 0,
mods_locked: 0, mods_locked: 0,
group: 0, group: 0,
}; });
emulate emulate.consume(event, client).await?;
.consume(Event::Keyboard(modifiers_event), client) Ok(())
.await; }
fn activate_client_if_exists(
client_manager: &mut ClientManager,
addr: SocketAddr,
last_ignored: &mut Option<SocketAddr>,
) -> Option<ClientHandle> {
let Some(handle) = client_manager.get_client(addr) else {
// log ignored if it is the first event from the client in a series
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr {
log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr);
}
return None;
};
// next event can be logged as ignored again
last_ignored.take();
let (_, client_state) = client_manager.get_mut(handle)?;
// reset ttl for client
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
Some(handle)
}
fn update_client_keys(
client_manager: &mut ClientManager,
handle: ClientHandle,
key: u32,
state: u8,
) -> (bool, bool) {
let Some(client_state) = client_manager.get_mut(handle).map(|(_, s)| s) else {
return (true, false);
};
// ignore double press / release events
let ignore_event = if state == 0 {
// ignore release event if key not pressed
!client_state.pressed_keys.remove(&key)
} else {
// ignore press event if key not released
!client_state.pressed_keys.insert(key)
};
let restart_timer = !client_state.pressed_keys.is_empty();
(ignore_event, restart_timer)
} }

View File

@@ -17,7 +17,7 @@ use tokio::{
}; };
use crate::{ use crate::{
client::{ClientEvent, ClientHandle, Position}, client::{ClientHandle, Position},
frontend::{self, FrontendEvent, FrontendListener, FrontendRequest}, frontend::{self, FrontendEvent, FrontendListener, FrontendRequest},
}; };
@@ -107,7 +107,8 @@ async fn handle_frontend_event(
log::debug!("frontend: {event:?}"); log::debug!("frontend: {event:?}");
match event { match event {
FrontendRequest::Create => { FrontendRequest::Create => {
add_client(server, frontend).await; let handle = add_client(server, frontend).await;
resolve_dns(server, resolve_tx, handle).await;
} }
FrontendRequest::Activate(handle, active) => { FrontendRequest::Activate(handle, active) => {
if active { if active {
@@ -140,10 +141,12 @@ async fn handle_frontend_event(
return true; return true;
} }
FrontendRequest::UpdateFixIps(handle, fix_ips) => { FrontendRequest::UpdateFixIps(handle, fix_ips) => {
update_fix_ips(server, resolve_tx, handle, fix_ips).await; update_fix_ips(server, handle, fix_ips).await;
resolve_dns(server, resolve_tx, handle).await;
} }
FrontendRequest::UpdateHostname(handle, hostname) => { FrontendRequest::UpdateHostname(handle, hostname) => {
update_hostname(server, resolve_tx, handle, hostname).await; update_hostname(server, resolve_tx, handle, hostname).await;
resolve_dns(server, resolve_tx, handle).await;
} }
FrontendRequest::UpdatePort(handle, port) => { FrontendRequest::UpdatePort(handle, port) => {
update_port(server, handle, port).await; update_port(server, handle, port).await;
@@ -152,31 +155,41 @@ async fn handle_frontend_event(
update_pos(server, handle, capture, emulate, pos).await; update_pos(server, handle, capture, emulate, pos).await;
} }
FrontendRequest::ResolveDns(handle) => { FrontendRequest::ResolveDns(handle) => {
let hostname = server resolve_dns(server, resolve_tx, handle).await;
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.hostname.clone());
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
} }
}; };
false false
} }
async fn resolve_dns(server: &Server, resolve_tx: &Sender<DnsRequest>, handle: ClientHandle) {
let hostname = server
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.hostname.clone());
if let Some(hostname) = hostname {
let _ = resolve_tx
.send(DnsRequest {
hostname: hostname.clone(),
handle,
})
.await;
}
}
async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) { async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) {
if let Err(e) = frontend.broadcast_event(event).await { if let Err(e) = frontend.broadcast_event(event).await {
log::error!("error notifying frontend: {e}"); log::error!("error notifying frontend: {e}");
} }
} }
pub async fn add_client(server: &Server, frontend: &mut FrontendListener) { pub async fn add_client(server: &Server, frontend: &mut FrontendListener) -> ClientHandle {
let handle = server.client_manager.borrow_mut().add_client(); let handle = server.client_manager.borrow_mut().add_client();
log::info!("added client {handle}"); log::info!("added client {handle}");
let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone(); let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone();
broadcast(frontend, FrontendEvent::Created(handle, c, s)).await; broadcast(frontend, FrontendEvent::Created(handle, c, s)).await;
handle
} }
pub async fn deactivate_client( pub async fn deactivate_client(
@@ -192,9 +205,8 @@ pub async fn deactivate_client(
None => return, None => return,
}; };
let event = ClientEvent::Destroy(handle); let _ = capture.send(CaptureEvent::Destroy(handle)).await;
let _ = capture.send(CaptureEvent::ClientEvent(event)).await; let _ = emulate.send(EmulationEvent::Destroy(handle)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(event)).await;
} }
pub async fn activate_client( pub async fn activate_client(
@@ -224,9 +236,8 @@ pub async fn activate_client(
}; };
/* notify emulation, capture and frontends */ /* notify emulation, capture and frontends */
let event = ClientEvent::Create(handle, pos); let _ = capture.send(CaptureEvent::Create(handle, pos.into())).await;
let _ = capture.send(CaptureEvent::ClientEvent(event)).await; let _ = emulate.send(EmulationEvent::Create(handle)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(event)).await;
} }
pub async fn remove_client( pub async fn remove_client(
@@ -245,31 +256,18 @@ pub async fn remove_client(
}; };
if active { if active {
let destroy = ClientEvent::Destroy(handle); let _ = capture.send(CaptureEvent::Destroy(handle)).await;
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await; let _ = emulate.send(EmulationEvent::Destroy(handle)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
} }
} }
async fn update_fix_ips( async fn update_fix_ips(server: &Server, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
server: &Server, let mut client_manager = server.client_manager.borrow_mut();
resolve_tx: &Sender<DnsRequest>, let Some((c, _)) = client_manager.get_mut(handle) else {
handle: ClientHandle, return;
fix_ips: Vec<IpAddr>,
) {
let hostname = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, _)) = client_manager.get_mut(handle) else {
return;
};
c.fix_ips = fix_ips;
c.hostname.clone()
}; };
if let Some(hostname) = hostname { c.fix_ips = fix_ips;
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
} }
async fn update_hostname( async fn update_hostname(
@@ -334,13 +332,11 @@ async fn update_pos(
// update state in event input emulator & input capture // update state in event input emulator & input capture
if changed { if changed {
if active { if active {
let destroy = ClientEvent::Destroy(handle); let _ = capture.send(CaptureEvent::Destroy(handle)).await;
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await; let _ = emulate.send(EmulationEvent::Destroy(handle)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
} }
let create = ClientEvent::Create(handle, pos); let _ = capture.send(CaptureEvent::Create(handle, pos.into())).await;
let _ = capture.send(CaptureEvent::ClientEvent(create)).await; let _ = emulate.send(EmulationEvent::Create(handle)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(create)).await;
} }
} }

View File

@@ -1,72 +1,44 @@
use std::net::SocketAddr; use std::{io, net::SocketAddr};
use anyhow::Result; use thiserror::Error;
use tokio::{ use tokio::{
net::UdpSocket, net::UdpSocket,
sync::mpsc::{Receiver, Sender}, sync::mpsc::{Receiver, Sender},
task::JoinHandle, task::JoinHandle,
}; };
use crate::{event::Event, frontend::FrontendEvent}; use crate::frontend::FrontendEvent;
use input_event::{Event, ProtocolError};
use super::Server; use super::Server;
pub async fn new( pub async fn new(
server: Server, server: Server,
frontend_notify_tx: Sender<FrontendEvent>, frontend_notify_tx: Sender<FrontendEvent>,
) -> Result<( ) -> io::Result<(
JoinHandle<()>, JoinHandle<()>,
Sender<(Event, SocketAddr)>, Sender<(Event, SocketAddr)>,
Receiver<Result<(Event, SocketAddr)>>, Receiver<Result<(Event, SocketAddr), NetworkError>>,
Sender<u16>, Sender<u16>,
)> { )> {
// bind the udp socket // bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get()); let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get());
let mut socket = UdpSocket::bind(listen_addr).await?; let mut socket = UdpSocket::bind(listen_addr).await?;
let (receiver_tx, receiver_rx) = tokio::sync::mpsc::channel(32); let (receiver_tx, receiver_rx) = tokio::sync::mpsc::channel(32);
let (sender_tx, mut sender_rx) = tokio::sync::mpsc::channel(32); let (sender_tx, sender_rx) = tokio::sync::mpsc::channel(32);
let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32); let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32);
let udp_task = tokio::task::spawn_local(async move { let udp_task = tokio::task::spawn_local(async move {
let mut sender_rx = sender_rx;
loop { loop {
let udp_receiver = udp_receiver(&socket, &receiver_tx);
let udp_sender = udp_sender(&socket, &mut sender_rx);
tokio::select! { tokio::select! {
event = receive_event(&socket) => { _ = udp_receiver => break, /* channel closed */
let _ = receiver_tx.send(event).await; _ = udp_sender => break, /* channel closed */
} port = port_rx.recv() => match port {
event = sender_rx.recv() => { Some(port) => update_port(&server, &frontend_notify_tx, &mut socket, port).await,
let Some((event, addr)) = event else { _ => break,
break;
};
if let Err(e) = send_event(&socket, event, addr) {
log::warn!("udp send failed: {e}");
};
}
port = port_rx.recv() => {
let Some(port) = port else {
break;
};
if socket.local_addr().unwrap().port() == port {
continue;
}
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
match UdpSocket::bind(listen_addr).await {
Ok(new_socket) => {
socket = new_socket;
server.port.replace(port);
let _ = frontend_notify_tx.send(FrontendEvent::PortChanged(port, None)).await;
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port();
let _ = frontend_notify_tx.send(FrontendEvent::PortChanged(
port,
Some(format!("could not change port: {e}")),
)).await;
}
}
} }
} }
} }
@@ -74,13 +46,73 @@ pub async fn new(
Ok((udp_task, sender_tx, receiver_rx, port_tx)) Ok((udp_task, sender_tx, receiver_rx, port_tx))
} }
async fn receive_event(socket: &UdpSocket) -> Result<(Event, SocketAddr)> { async fn update_port(
server: &Server,
frontend_chan: &Sender<FrontendEvent>,
socket: &mut UdpSocket,
port: u16,
) {
// if port is the same, we dont need to change it
if socket.local_addr().unwrap().port() == port {
return;
}
// create new socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
let frontend_event = match UdpSocket::bind(listen_addr).await {
Ok(new_socket) => {
*socket = new_socket;
server.port.replace(port);
FrontendEvent::PortChanged(port, None)
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port();
FrontendEvent::PortChanged(port, Some(format!("could not change port: {e}")))
}
};
let _ = frontend_chan.send(frontend_event).await;
}
async fn udp_receiver(
socket: &UdpSocket,
receiver_tx: &Sender<Result<(Event, SocketAddr), NetworkError>>,
) {
loop {
let event = receive_event(socket).await;
if receiver_tx.send(event).await.is_err() {
break;
}
}
}
async fn udp_sender(socket: &UdpSocket, rx: &mut Receiver<(Event, SocketAddr)>) {
loop {
let (event, addr) = match rx.recv().await {
Some(e) => e,
None => return,
};
if let Err(e) = send_event(socket, event, addr) {
log::warn!("udp send failed: {e}");
};
}
}
#[derive(Debug, Error)]
pub(crate) enum NetworkError {
#[error(transparent)]
Protocol(#[from] ProtocolError),
#[error("network error: `{0}`")]
Io(#[from] io::Error),
}
async fn receive_event(socket: &UdpSocket) -> Result<(Event, SocketAddr), NetworkError> {
let mut buf = vec![0u8; 22]; let mut buf = vec![0u8; 22];
let (_amt, src) = socket.recv_from(&mut buf).await?; let (_amt, src) = socket.recv_from(&mut buf).await?;
Ok((Event::try_from(buf)?, src)) Ok((Event::try_from(buf)?, src))
} }
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> { fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize, NetworkError> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string()); log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let data: Vec<u8> = (&e).into(); let data: Vec<u8> = (&e).into();
// When udp blocks, we dont want to block the event loop. // When udp blocks, we dont want to block the event loop.

View File

@@ -5,7 +5,9 @@ use tokio::{
task::JoinHandle, task::JoinHandle,
}; };
use crate::{client::ClientHandle, event::Event}; use input_event::Event;
use crate::client::ClientHandle;
use super::{capture_task::CaptureEvent, emulation_task::EmulationEvent, Server, State}; use super::{capture_task::CaptureEvent, emulation_task::EmulationEvent, Server, State};