Compare commits

..

1 Commits

Author SHA1 Message Date
Ferdinand Schober
9424abbd56 basic enter hook command 2024-05-12 02:09:43 +02:00
89 changed files with 5841 additions and 8255 deletions

View File

@@ -55,9 +55,7 @@ jobs:
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
pipx install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"

View File

@@ -66,9 +66,7 @@ jobs:
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
pipx install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"

View File

@@ -51,9 +51,7 @@ jobs:
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
pipx install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"

1646
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,74 @@
[workspace]
members = [
"input-capture",
"input-emulation",
"input-event",
"lan-mouse-ipc",
"lan-mouse-cli",
"lan-mouse-gtk",
"lan-mouse-proto",
]
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.10.0"
version = "0.7.3"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/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]
strip = true
lto = "fat"
[dependencies]
input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
tempfile = "3.8"
hickory-resolver = "0.24.1"
memmap = "0.7"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71"
log = "0.4.20"
env_logger = "0.11.3"
serde_json = "1.0.107"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"process",
"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"
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 }
adw = { package = "libadwaita", version = "0.6.0", features = ["v1_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"
slab = "0.4.9"
thiserror = "2.0.0"
tokio-util = "0.7.11"
local-channel = "0.1.5"
[target.'cfg(unix)'.dependencies]
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]
glib-build-tools = { version = "0.19.0", optional = true }
[features]
default = [
"gtk",
"layer_shell_capture",
"x11_capture",
"libei_capture",
"wlroots_emulation",
"libei_emulation",
"rdp_emulation",
"x11_emulation",
]
gtk = ["dep:lan-mouse-gtk"]
layer_shell_capture = ["input-capture/layer_shell"]
x11_capture = ["input-capture/x11"]
libei_capture = ["input-event/libei", "input-capture/libei"]
libei_emulation = ["input-event/libei", "input-emulation/libei"]
wlroots_emulation = ["input-emulation/wlroots"]
x11_emulation = ["input-emulation/x11"]
rdp_emulation = ["input-emulation/remote_desktop_portal"]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
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"]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"]

330
README.md
View File

@@ -1,107 +1,110 @@
# Lan Mouse
Lan Mouse is a *cross-platform* mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple PCs via a single set of mouse and keyboard.
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.
Goal of this project is to be an open-source alternative to proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/)
and an alternative to other open source tools like [Deskflow](https://github.com/deskflow/deskflow) or [Input Leap](https://github.com/input-leap) (Synergy fork).
Focus lies on performance, ease of use and a maintainable implementation that can be expanded to support additional backends for e.g. Android, iOS, ... in the future.
***blazingly fast™*** because it's written in rust.
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).
- _Now with a gtk frontend_
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/screenshots/dark.png?raw=true">
<source media="(prefers-color-scheme: light)" srcset="/screenshots/light.png?raw=true">
<img alt="Screenshot of Lan-Mouse" srcset="/screenshots/dark.png">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709">
<img alt="Screenshot of Lan-Mouse" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
</picture>
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
***blazingly fast™*** because it's written in rust.
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
> [!WARNING]
> DISCLAIMER:
> Until [#200](https://github.com/feschber/lan-mouse/pull/200) is merged, all network traffic is **unencrypted** and sent in **plaintext**!
> Since this tool has gained a bit of popularity over the past couple of days:
>
> All network traffic is currently **unencrypted** and sent in **plaintext**.
>
> A malicious actor with access to the network could read input data or send input events with spoofed IPs to take control over a device.
>
> Therefore you should only use this tool in your local network with trusted devices.
> I take no responsibility for any security breaches!
> Therefore you should only use this tool in your local network with trusted devices for now
> and I take no responsibility for any leakage of data!
## OS Support
Most current desktop environments and operating systems are fully supported, this includes
- GNOME >= 45
- KDE Plasma >= 6.1
- Most wlroots based compositors, including Sway (>= 1.8), Hyprland and Wayfire
- Windows
- MacOS
The following table shows support for input emulation (to emulate events received from other clients) and
input capture (to send events *to* other clients) on different operating systems:
### Caveats / Known Issues
| OS / Desktop Environment | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: (starting at GNOME 45) |
| Windows | :heavy_check_mark: | :heavy_check_mark: |
| X11 | :heavy_check_mark: | WIP |
| MacOS | :heavy_check_mark: | WIP |
> [!Important]
> - **X11** currently only has support for input emulation, i.e. can only be used on the receiving end.
> Gnome -> Sway only partially works (modifier events are not handled correctly)
> [!Important]
> **Wayfire**
>
> - **Sway / wlroots**: Wlroots based compositors without libei support on the receiving end currently do not handle modifier events on the client side.
> This results in CTRL / SHIFT / ALT / SUPER keys not working with a sending device that is NOT using the `layer-shell` backend
>
> - **Wayfire**: If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
>
> - **Windows**: The mouse cursor will be invisible when sending input to a Windows system if
> there is no real mouse connected to the machine.
For more detailed information about os support see [Detailed OS Support](#detailed-os-support)
## Installation
<details>
<summary>Arch Linux</summary>
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
### Install via cargo
```sh
pacman -S lan-mouse
cargo install lan-mouse
```
The prerelease version (following `main`) is available on the AUR:
### Download from Releases
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux
Lan Mouse is available on the AUR:
```sh
# git version (includes latest changes)
paru -S lan-mouse-git
# alternatively
paru -S lan-mouse-bin
```
</details>
<details>
<summary>Nix (OS)</summary>
### Nix
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
- flake: [README.md](./nix/README.md)
</details>
<details>
<summary>Manual Installation</summary>
### Manual Installation
First make sure to [install the necessary dependencies](#installing-dependencies).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
Alternatively, the `lan-mouse` binary can be compiled from source (see below).
### Installing desktop file, app icon and firewall rules (optional)
Build in release mode:
```sh
# install lan-mouse (replace path/to/ with the correct path)
sudo cp path/to/lan-mouse /usr/local/bin/
cargo build --release
```
Run directly:
```sh
cargo run --release
```
Install the files:
```sh
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
# install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
sudo cp lan-mouse-gtk/resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
sudo cp resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
# update icon cache
gtk-update-icon-cache /usr/local/share/icons/hicolor/
@@ -115,34 +118,17 @@ sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings
```
Instead of downloading from the releases, the `lan-mouse` binary
can be easily compiled via cargo or nix:
### Conditional Compilation
### Compiling and installing manually:
```sh
# compile in release mode
cargo build --release
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
```
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
### Compiling and installing via cargo:
```sh
# will end up in ~/.cargo/bin
cargo install lan-mouse
```
### Compiling and installing via nix:
```sh
# you can find the executable in result/bin/lan-mouse
nix-build
```
### Conditional compilation
Support for other platforms is omitted automatically based on the active
rust toolchain.
Additionally, available backends and frontends can be configured manually via
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
@@ -151,17 +137,14 @@ an executable with just support for wayland:
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
</details>
## Installing Dependencies for Development / Compiling from Source
## Installing Dependencies
<details>
<summary>MacOS</summary>
```sh
brew install libadwaita pkg-config
brew install libadwaita
```
</details>
@@ -188,24 +171,12 @@ sudo pacman -S libadwaita gtk libx11 libxtst
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
</details>
<details>
<summary>Nix</summary>
```sh
nix-shell .
```
</details>
<details>
<summary>Nix (flake)</summary>
```sh
nix develop
```
</details>
<details>
<summary>Windows</summary>
> [!NOTE]
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
- First install [Rust](https://www.rust-lang.org/tools/install).
- Then follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
@@ -236,20 +207,18 @@ python -m pipx ensurepath
pipx install gvsbuild
# build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg adwaita-icon-theme
gvsbuild build gtk4 libadwaita librsvg
```
- **Make sure to add the directory** `C:\gtk-build\gtk\x64\release\bin`
[**to the `PATH` environment variable**]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation).
the gtk frontend (see conditional compilation below).
</details>
## Usage
<details>
<summary>Gtk Frontend</summary>
### Gtk Frontend
By default the gtk frontend will open when running `lan-mouse`.
To add a new connection, simply click the `Add` button on *both* devices,
@@ -257,11 +226,8 @@ enter the corresponding hostname and activate it.
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
opened up in your firewall.
</details>
<details>
<summary>Command Line Interface</summary>
### Command Line Interface
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
@@ -275,17 +241,13 @@ $ cargo run --release -- --frontend cli
(...)
> activate 0
```
</details>
<details>
<summary>Daemon Mode</summary>
Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service).
### Daemon
Lan Mouse can be launched in daemon mode to keep it running in the background.
To do so, add `--daemon` to the commandline args:
```sh
lan-mouse --daemon
$ cargo run --release -- --daemon
```
In order to start lan-mouse with a graphical session automatically,
@@ -298,7 +260,6 @@ cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service
```
</details>
## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
@@ -354,60 +315,109 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
- [x] Liveness tracking: Automatically release keys, when server offline
- [x] MacOS KeyCode Translation
- [x] Libei Input Capture
- [x] MacOS Input Capture
- [x] Windows Input Capture
- [ ] *Encryption* (WIP)
- [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [ ] *Encryption*
## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
Each event is sent as one single datagram, currently without any acknowledgement to guarantee 0% packet loss.
This means, any packet that is lost results in a discarded mouse / key event, which is ignored for now.
**UDP** also has the additional benefit that no reconnection logic is required.
Any client can just go offline and it will simply start working again as soon as it comes back online.
Additionally a tcp server is hosted for data that needs to be sent reliably (e.g. the keymap from the server or clipboard contents in the future) can be requested via a tcp connection.
## Bandwidth considerations
The most bandwidth is taken up by mouse events. A typical office mouse has a polling rate of 125Hz
while gaming mice typically have a much higher polling rate of 1000Hz.
A mouse Event consists of 21 Bytes:
- 1 Byte for the event type enum,
- 4 Bytes (u32) for the timestamp,
- 8 Bytes (f64) for dx,
- 8 Bytes (f64) for dy.
Additionally the IP header with 20 Bytes and the udp header with 8 Bytes take up another 28 Byte.
So in total there is 49 * 1000 Bytes/s for a 1000Hz gaming mouse.
This makes for a bandwidth requirement of 392 kbit/s in total _even_ for a high end gaming mouse.
So bandwidth is a non-issue.
Larger data chunks, like the keymap are offered by the server via tcp listening on the same port.
This way we dont need to implement any congestion control and leave this up to tcp.
In the future this can be used for e.g. clipboard contents as well.
## Packets per Second
While on LAN the performance is great,
some WIFI cards seem to struggle with the amount of packets per second,
particularly on high-end gaming mice with 1000Hz+ polling rates.
The plan is to implement a way of accumulating packets and sending them as
one single key event to reduce the packet rate (basically reducing the polling
rate artificially).
The way movement data is currently sent is also quite wasteful since even a 16bit integer
is likely enough to represent even the fastest possible mouse movement.
A different encoding that is more efficient for smaller values like
[Protocol Buffers](https://protobuf.dev/programming-guides/encoding/)
would be a better choice for the future and could also help for WIFI connections.
## Security
Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes.
- There should be an encryption layer below the application to enable a secure link.
- The encryption keys could be generated by the graphical frontend.
## Detailed OS Support
## Wayland support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
In order to use a device for sending events, an **input-capture** backend is required, while receiving events requires
a supported **input-emulation** *and* **input-capture** backend.
For this reason a suitable backend is chosen based on the active desktop environment / compositor.
A suitable backend is chosen automatically based on the active desktop environment / compositor.
Different compositors have different ways of enabling input emulation:
The following sections detail the emulation and capture backends provided by lan-mouse and their support in desktop environments / operating systems.
#### Wlroots
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1)
### Input Emulation Support
#### KDE
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)),
it is however not exposed to third party applications.
| Desktop / Backend | layer-shell | libei | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| Wayland (wlroots) | :heavy_check_mark: | | | | |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | | | |
| Windows | | | :heavy_check_mark: | | |
| MacOS | | | | :heavy_check_mark: | |
| X11 | | | | | WIP |
The recommended way to emulate input on KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
- `wlroots`: This backend makes use of the [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1) protocols and is supported by most wlroots based compositors.
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
- `xdp`: This backend uses the [freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop) and is supported on GNOME and Plasma.
- `x11`: Backend for X11 sessions.
- `windows`: Backend for Windows.
- `macos`: Backend for MacOS.
#### Gnome
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation and capture,
which has the goal to become the general approach for emulating and capturing Input on Wayland.
### Input capture
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
### Input Capture Support
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
| Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
| Wayland (wlroots) | :heavy_check_mark: | | | | | |
| Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Windows | | | | :heavy_check_mark: | | |
| MacOS | | | | | :heavy_check_mark: | |
| X11 | | | | | | :heavy_check_mark: |
- `layer-shell`: This backend creates a single pixel wide window on the edges of Displays to capture the cursor using the [layer-shell protocol](https://wayland.app/protocols/wlr-layer-shell-unstable-v1).
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
- `windows`: Backend for input capture on Windows.
- `macos`: Backend for input capture on MacOS.
- `x11`: TODO (not yet supported)
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)

View File

@@ -1,15 +1,9 @@
use std::process::Command;
fn main() {
// commit hash
let git_describe = Command::new("git")
.arg("describe")
.arg("--always")
.arg("--dirty")
.arg("--tags")
.output()
.unwrap();
let git_describe = String::from_utf8(git_describe.stdout).unwrap();
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}");
// composite_templates
#[cfg(feature = "gtk")]
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresource.xml",
"lan-mouse.gresource",
);
}

View File

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

View File

@@ -1,3 +0,0 @@
{ pkgs ? import <nixpkgs> { }
}:
pkgs.callPackage nix/default.nix { }

46
flake.lock generated
View File

@@ -1,12 +1,30 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1728018373,
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"lastModified": 1710806803,
"narHash": "sha256-qrxvLS888pNJFwJdK+hf1wpRCSQcqA6W5+Ox202NDa0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"rev": "b06025f1533a1e07b6db3e75151caa155d1c7eb3",
"type": "github"
},
"original": {
@@ -24,16 +42,17 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1728181869,
"narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
"lastModified": 1710987136,
"narHash": "sha256-Q8GRdlAIKZ8tJUXrbcRO1pA33AdoPfTUirsSnmGQnOU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
"rev": "97596b54ac34ad8184ca1eef44b1ec2e5c2b5f9e",
"type": "github"
},
"original": {
@@ -41,6 +60,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -16,7 +16,6 @@
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-darwin"
"aarch64-linux"
"x86_64-darwin"
"x86_64-linux"
];
@@ -51,13 +50,10 @@
xorg.libX11
gtk4
libadwaita
librsvg
xorg.libXtst
] ++ lib.optionals stdenv.isDarwin
(with darwin.apple_sdk_11_0.frameworks; [
CoreGraphics
ApplicationServices
]);
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk_11_0.frameworks.CoreGraphics
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};

View File

@@ -1,75 +0,0 @@
[package]
name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.28"
futures-core = "0.3.30"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.3.0" }
memmap = "0.7"
tempfile = "3.8"
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"process",
"rt",
"sync",
"signal",
] }
once_cell = "1.19.0"
async-trait = "0.1.81"
tokio-util = "0.7.11"
[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.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.24.0", features = ["highsierra"] }
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
libc = "0.2.155"
keycode = "0.4.0"
bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[features]
default = ["layer_shell", "x11", "libei"]
layer_shell = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",
]
x11 = ["dep:x11"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -1,86 +0,0 @@
use std::f64::consts::PI;
use std::pin::Pin;
use std::task::{ready, Context, Poll};
use std::time::Duration;
use async_trait::async_trait;
use futures_core::Stream;
use input_event::PointerEvent;
use tokio::time::{self, Instant, Interval};
use super::{Capture, CaptureError, CaptureEvent, Position};
pub struct DummyInputCapture {
start: Option<Instant>,
interval: Interval,
offset: (i32, i32),
}
impl DummyInputCapture {
pub fn new() -> Self {
Self {
start: None,
interval: time::interval(Duration::from_millis(1)),
offset: (0, 0),
}
}
}
impl Default for DummyInputCapture {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Capture for DummyInputCapture {
async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0;
impl Stream for DummyInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let current = ready!(self.interval.poll_tick(cx));
let event = match self.start {
None => {
self.start.replace(current);
CaptureEvent::Begin
}
Some(start) => {
let elapsed = start.elapsed();
let elapsed_sec_f64 = elapsed.as_secs_f64();
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
let offset = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
let offset = (offset.0 as i32, offset.1 as i32);
let relative_motion = (offset.0 - self.offset.0, offset.1 - self.offset.1);
self.offset = offset;
let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
CaptureEvent::Input(input_event::Event::Pointer(PointerEvent::Motion {
time: 0,
dx,
dy,
}))
}
};
Poll::Ready(Some(Ok((Position::Left, event))))
}
}

View File

@@ -1,157 +0,0 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InputCaptureError {
#[error("error creating input-capture: `{0}`")]
Create(#[from] CaptureCreationError),
#[error("error while capturing input: `{0}`")]
Capture(#[from] CaptureError),
}
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use std::io;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use ashpd::desktop::ResponseError;
#[cfg(target_os = "macos")]
use core_graphics::base::CGError;
#[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("libei error: `{0}`")]
Reis(#[from] reis::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error(transparent)]
Portal(#[from] ashpd::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei disconnected - reason: `{0}`")]
Disconnected(String),
#[cfg(target_os = "macos")]
#[error("failed to warp mouse cursor: `{0}`")]
WarpCursor(CGError),
#[cfg(target_os = "macos")]
#[error("reset_mouse_position called without a connected client")]
ResetMouseWithoutClient,
#[cfg(target_os = "macos")]
#[error("core-graphics error: {0}")]
CoreGraphics(CGError),
#[cfg(target_os = "macos")]
#[error("unable to map key event: {0}")]
KeyMapError(i64),
#[cfg(target_os = "macos")]
#[error("Event tap disabled")]
EventTapDisabled,
}
#[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 = "layer_shell", 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(windows)]
#[error("error creating windows capture backend")]
Windows,
#[cfg(target_os = "macos")]
#[error("error creating macos capture backend: `{0}`")]
MacOS(#[from] MacosCaptureCreationError),
}
impl CaptureCreationError {
/// request was intentionally denied by the user
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub(crate) fn cancelled_by_user(&self) -> bool {
matches!(
self,
CaptureCreationError::Libei(LibeiCaptureCreationError::Ashpd(ashpd::Error::Response(
ResponseError::Cancelled
)))
)
}
#[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))]
pub(crate) fn cancelled_by_user(&self) -> bool {
false
}
}
#[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 = "layer_shell", 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 = "layer_shell", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "layer_shell", 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 MacosCaptureCreationError {
#[error("event source creation failed!")]
EventSourceCreation,
#[cfg(target_os = "macos")]
#[error("event tap creation failed")]
EventTapCreation,
#[error("failed to set CG Cursor property")]
CGCursorProperty,
#[cfg(target_os = "macos")]
#[error("failed to get display ids: {0}")]
ActiveDisplays(CGError),
}

View File

@@ -1,338 +0,0 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Display,
mem::swap,
task::{ready, Poll},
};
use async_trait::async_trait;
use futures::StreamExt;
use futures_core::Stream;
use input_event::{scancode, Event, KeyboardEvent};
pub use error::{CaptureCreationError, CaptureError, InputCaptureError};
pub mod error;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
mod layer_shell;
#[cfg(windows)]
mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11;
/// fallback input capture (does not produce events)
mod dummy;
pub type CaptureHandle = u64;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum CaptureEvent {
/// capture on this capture handle is now active
Begin,
/// input event coming from capture handle
Input(Event),
}
impl Display for CaptureEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CaptureEvent::Begin => write!(f, "begin capture"),
CaptureEvent::Input(e) => write!(f, "{e}"),
}
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum Position {
Left,
Right,
Top,
Bottom,
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
Position::Left => Self::Right,
Position::Right => Self::Left,
Position::Top => Self::Bottom,
Position::Bottom => Self::Top,
}
}
}
impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pos = match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
};
write!(f, "{}", pos)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
MacOs,
Dummy,
}
impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"),
#[cfg(windows)]
Backend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
Backend::MacOs => write!(f, "MacOS"),
Backend::Dummy => write!(f, "dummy"),
}
}
}
pub struct InputCapture {
/// capture backend
capture: Box<dyn Capture>,
/// keys pressed by active capture
pressed_keys: HashSet<scancode::Linux>,
/// map from position to ids
position_map: HashMap<Position, Vec<CaptureHandle>>,
/// map from id to position
id_map: HashMap<CaptureHandle, Position>,
/// pending events
pending: VecDeque<(CaptureHandle, CaptureEvent)>,
}
impl InputCapture {
/// create a new client with the given id
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
assert!(!self.id_map.contains_key(&id));
self.id_map.insert(id, pos);
if let Some(v) = self.position_map.get_mut(&pos) {
v.push(id);
Ok(())
} else {
self.position_map.insert(pos, vec![id]);
self.capture.create(pos).await
}
}
/// destroy the client with the given id, if it exists
pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> {
let pos = self
.id_map
.remove(&id)
.expect("no position for this handle");
log::debug!("destroying capture {id} @ {pos}");
let remaining = self.position_map.get_mut(&pos).expect("id vector");
remaining.retain(|&i| i != id);
log::debug!("remaining ids @ {pos}: {remaining:?}");
if remaining.is_empty() {
log::debug!("destroying capture @ {pos} - no remaining ids");
self.position_map.remove(&pos);
self.capture.destroy(pos).await?;
}
Ok(())
}
/// release mouse
pub async fn release(&mut self) -> Result<(), CaptureError> {
self.pressed_keys.clear();
self.capture.release().await
}
/// destroy the input capture
pub async fn terminate(&mut self) -> Result<(), CaptureError> {
self.capture.terminate().await
}
/// creates a new [`InputCapture`]
pub async fn new(backend: Option<Backend>) -> Result<Self, CaptureCreationError> {
let capture = create(backend).await?;
Ok(Self {
capture,
id_map: Default::default(),
pending: Default::default(),
position_map: Default::default(),
pressed_keys: HashSet::new(),
})
}
/// check whether the given keys are pressed
pub fn keys_pressed(&self, keys: &[scancode::Linux]) -> bool {
keys.iter().all(|k| self.pressed_keys.contains(k))
}
fn update_pressed_keys(&mut self, key: u32, state: u8) {
if let Ok(scancode) = scancode::Linux::try_from(key) {
log::debug!("key: {key}, state: {state}, scancode: {scancode:?}");
match state {
1 => self.pressed_keys.insert(scancode),
_ => self.pressed_keys.remove(&scancode),
};
}
}
}
impl Stream for InputCapture {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(e) = self.pending.pop_front() {
return Poll::Ready(Some(Ok(e)));
}
// ready
let event = ready!(self.capture.poll_next_unpin(cx));
// stream closed
let event = match event {
Some(e) => e,
None => return Poll::Ready(None),
};
// error occurred
let (pos, event) = match event {
Ok(e) => e,
Err(e) => return Poll::Ready(Some(Err(e))),
};
// handle key presses
if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key, state, .. })) = event {
self.update_pressed_keys(key, state);
}
let len = self
.position_map
.get(&pos)
.map(|ids| ids.len())
.unwrap_or(0);
match len {
0 => Poll::Pending,
1 => Poll::Ready(Some(Ok((
self.position_map.get(&pos).expect("no id")[0],
event,
)))),
_ => {
let mut position_map = HashMap::new();
swap(&mut self.position_map, &mut position_map);
{
for &id in position_map.get(&pos).expect("position") {
self.pending.push_back((id, event));
}
}
swap(&mut self.position_map, &mut position_map);
Poll::Ready(Some(Ok(self.pending.pop_front().expect("event"))))
}
}
}
}
#[async_trait]
trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + Unpin {
/// create a new client with the given id
async fn create(&mut self, pos: Position) -> Result<(), CaptureError>;
/// destroy the client with the given id, if it exists
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError>;
/// release mouse
async fn release(&mut self) -> Result<(), CaptureError>;
/// destroy the input capture
async fn terminate(&mut self) -> Result<(), CaptureError>;
}
async fn create_backend(
backend: Backend,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError,
> {
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)]
Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())),
#[cfg(target_os = "macos")]
Backend::MacOs => Ok(Box::new(macos::MacOSInputCapture::new().await?)),
Backend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())),
}
}
async fn create(
backend: Option<Backend>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError,
> {
if let Some(backend) = backend {
let b = create_backend(backend).await;
if b.is_ok() {
log::info!("using capture backend: {backend}");
}
return b;
}
for backend in [
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,
#[cfg(windows)]
Backend::Windows,
#[cfg(target_os = "macos")]
Backend::MacOs,
] {
match create_backend(backend).await {
Ok(b) => {
log::info!("using capture backend: {backend}");
return Ok(b);
}
Err(e) if e.cancelled_by_user() => return Err(e),
Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"),
}
}
Err(CaptureCreationError::NoAvailableBackend)
}

View File

@@ -1,618 +0,0 @@
use ashpd::{
desktop::{
input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones,
},
Session,
},
enumflags2::BitFlags,
};
use async_trait::async_trait;
use futures::{FutureExt, StreamExt};
use reis::{
ei::{self, handshake::ContextType},
event::{Connection, DeviceCapability, EiEvent},
tokio::EiConvertEventStream,
};
use std::{
cell::Cell,
collections::HashMap,
io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::Arc,
task::{Context, Poll},
};
use tokio::{
sync::{
mpsc::{self, Receiver, Sender},
Notify,
},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use futures_core::Stream;
use input_event::Event;
use crate::CaptureEvent;
use super::{
error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, Position,
};
/* 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 be recreated when the barriers are updated */
/// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent {
Create(Position),
Destroy(Position),
}
#[allow(dead_code)]
pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>,
capture_task: JoinHandle<Result<(), CaptureError>>,
event_rx: Receiver<(Position, CaptureEvent)>,
notify_capture: Sender<LibeiNotifyEvent>,
notify_release: Arc<Notify>,
cancellation_token: CancellationToken,
terminated: bool,
}
/// returns (start pos, end pos), inclusive
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
let (x, y) = (r.x_offset(), r.y_offset());
let (w, h) = (r.width() as i32, r.height() as i32);
match pos {
Position::Left => (x, y, x, y + h - 1),
Position::Right => (x + w, y, x + w, y + h - 1),
Position::Top => (x, y, x + w - 1, y),
Position::Bottom => (x, y + h, x + w - 1, y + h),
}
}
/// Ashpd does not expose fields
#[derive(Clone, Copy, Debug)]
struct ICBarrier {
barrier_id: BarrierID,
position: (i32, i32, i32, i32),
}
impl ICBarrier {
fn new(barrier_id: BarrierID, position: (i32, i32, i32, i32)) -> Self {
Self {
barrier_id,
position,
}
}
}
impl From<ICBarrier> for Barrier {
fn from(barrier: ICBarrier) -> Self {
Barrier::new(barrier.barrier_id, barrier.position)
}
}
fn select_barriers(
zones: &Zones,
clients: &[Position],
next_barrier_id: &mut NonZeroU32,
) -> (Vec<ICBarrier>, HashMap<BarrierID, Position>) {
let mut pos_for_barrier = HashMap::new();
let mut barriers: Vec<ICBarrier> = vec![];
for pos in clients {
let mut client_barriers = zones
.regions()
.iter()
.map(|r| {
let id = *next_barrier_id;
*next_barrier_id = next_barrier_id
.checked_add(1)
.expect("barrier id out of range");
let position = pos_to_barrier(r, *pos);
pos_for_barrier.insert(id, *pos);
ICBarrier::new(id, position)
})
.collect();
barriers.append(&mut client_barriers);
}
(barriers, pos_for_barrier)
}
async fn update_barriers(
input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>,
active_clients: &[Position],
next_barrier_id: &mut NonZeroU32,
) -> Result<(Vec<ICBarrier>, HashMap<BarrierID, Position>), ashpd::Error> {
let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}");
let (barriers, id_map) = select_barriers(&zones, active_clients, next_barrier_id);
log::debug!("barriers: {barriers:?}");
log::debug!("client for barrier id: {id_map:?}");
let ashpd_barriers: Vec<Barrier> = barriers.iter().copied().map(|b| b.into()).collect();
let response = input_capture
.set_pointer_barriers(session, &ashpd_barriers, zones.zone_set())
.await?;
let response = response.response()?;
log::debug!("{response:?}");
Ok((barriers, id_map))
}
async fn create_session<'a>(
input_capture: &'a InputCapture<'a>,
) -> std::result::Result<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session");
input_capture
.create_session(
None,
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
)
.await
}
async fn connect_to_eis(
input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>,
) -> Result<(ei::Context, Connection, EiConvertEventStream), CaptureError> {
log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?;
// create unix stream from fd
let stream = UnixStream::from(fd);
stream.set_nonblocking(true)?;
// create ei context
let context = ei::Context::new(stream)?;
let (conn, event_stream) = context
.handshake_tokio("de.feschber.LanMouse", ContextType::Receiver)
.await?;
Ok((context, conn, event_stream))
}
async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream,
context: ei::Context,
event_tx: Sender<(Position, CaptureEvent)>,
release_session: Arc<Notify>,
current_pos: Rc<Cell<Option<Position>>>,
) -> Result<(), CaptureError> {
loop {
let ei_event = ei_event_stream
.next()
.await
.ok_or(CaptureError::EndOfStream)??;
log::trace!("from ei: {ei_event:?}");
let client = current_pos.get();
handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?;
}
}
impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
let (event_tx, event_rx) = mpsc::channel(1);
let (notify_capture, notify_rx) = mpsc::channel(1);
let notify_release = Arc::new(Notify::new());
let cancellation_token = CancellationToken::new();
let capture = do_capture(
input_capture_ptr,
notify_rx,
notify_release.clone(),
first_session,
event_tx,
cancellation_token.clone(),
);
let capture_task = tokio::task::spawn_local(capture);
let producer = Self {
input_capture,
event_rx,
capture_task,
notify_capture,
notify_release,
cancellation_token,
terminated: false,
};
Ok(producer)
}
}
async fn do_capture(
input_capture: *const InputCapture<'static>,
mut capture_event: Receiver<LibeiNotifyEvent>,
notify_release: Arc<Notify>,
session: Option<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>)>,
event_tx: Sender<(Position, CaptureEvent)>,
cancellation_token: CancellationToken,
) -> Result<(), CaptureError> {
let mut session = session.map(|s| s.0);
/* safety: libei_task does not outlive Self */
let input_capture = unsafe { &*input_capture };
let mut active_clients: Vec<Position> = vec![];
let mut next_barrier_id = NonZeroU32::new(1).expect("id must be non-zero");
let mut zones_changed = input_capture.receive_zones_changed().await?;
loop {
// do capture session
let cancel_session = CancellationToken::new();
let cancel_update = CancellationToken::new();
let mut capture_event_occured: Option<LibeiNotifyEvent> = None;
let mut zones_have_changed = false;
// kill session if clients need to be updated
let handle_session_update_request = async {
tokio::select! {
_ = cancellation_token.cancelled() => {
log::debug!("cancelled")
}, /* exit requested */
_ = cancel_update.cancelled() => {
log::debug!("update task cancelled");
}, /* session exited */
_ = zones_changed.next() => {
log::debug!("zones changed!");
zones_have_changed = true
}, /* zones have changed */
e = capture_event.recv() => if let Some(e) = e { /* clients changed */
log::debug!("capture event: {e:?}");
capture_event_occured.replace(e);
},
}
// kill session (might already be dead!)
log::debug!("=> cancelling session");
cancel_session.cancel();
};
if !active_clients.is_empty() {
// create session
let mut session = match session.take() {
Some(s) => s,
None => create_session(input_capture).await?.0,
};
let capture_session = do_capture_session(
input_capture,
&mut session,
&event_tx,
&active_clients,
&mut next_barrier_id,
&notify_release,
(cancel_session.clone(), cancel_update.clone()),
);
let (capture_result, ()) = tokio::join!(capture_session, handle_session_update_request);
log::debug!("capture session + session_update task done!");
// disable capture
log::debug!("disabling input capture");
if let Err(e) = input_capture.disable(&session).await {
log::warn!("input_capture.disable(&session) {e}");
}
if let Err(e) = session.close().await {
log::warn!("session.close(): {e}");
}
// propagate error from capture session
capture_result?;
} else {
handle_session_update_request.await;
}
// update clients if requested
if let Some(event) = capture_event_occured.take() {
match event {
LibeiNotifyEvent::Create(p) => active_clients.push(p),
LibeiNotifyEvent::Destroy(p) => active_clients.retain(|&pos| pos != p),
}
}
// break
if cancellation_token.is_cancelled() {
break Ok(());
}
}
}
async fn do_capture_session(
input_capture: &InputCapture<'_>,
session: &mut Session<'_, InputCapture<'_>>,
event_tx: &Sender<(Position, CaptureEvent)>,
active_clients: &[Position],
next_barrier_id: &mut NonZeroU32,
notify_release: &Notify,
cancel: (CancellationToken, CancellationToken),
) -> Result<(), CaptureError> {
let (cancel_session, cancel_update) = cancel;
// current client
let current_pos = Rc::new(Cell::new(None));
// connect to eis server
let (context, _conn, ei_event_stream) = connect_to_eis(input_capture, session).await?;
// set barriers
let (barriers, pos_for_barrier_id) =
update_barriers(input_capture, session, active_clients, next_barrier_id).await?;
log::debug!("enabling session");
input_capture.enable(session).await?;
// cancellation token to release session
let release_session = Arc::new(Notify::new());
// async event task
let cancel_ei_handler = CancellationToken::new();
let event_chan = event_tx.clone();
let pos = current_pos.clone();
let cancel_session_clone = cancel_session.clone();
let release_session_clone = release_session.clone();
let cancel_ei_handler_clone = cancel_ei_handler.clone();
let ei_task = async move {
tokio::select! {
r = libei_event_handler(
ei_event_stream,
context,
event_chan,
release_session_clone,
pos,
) => {
log::debug!("libei exited: {r:?} cancelling session task");
cancel_session_clone.cancel();
}
_ = cancel_ei_handler_clone.cancelled() => {},
}
Ok::<(), CaptureError>(())
};
let capture_session_task = async {
// receiver for activation tokens
let mut activated = input_capture.receive_activated().await?;
let mut ei_devices_changed = false;
loop {
tokio::select! {
activated = activated.next() => {
let activated = activated.ok_or(CaptureError::ActivationClosed)?;
log::debug!("activated: {activated:?}");
// get barrier id from activation
let barrier_id = match activated.barrier_id() {
Some(ActivatedBarrier::Barrier(id)) => id,
// workaround for KDE plasma not reporting barrier ids
Some(ActivatedBarrier::UnknownBarrier) | None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")),
};
// find client corresponding to barrier
let pos = *pos_for_barrier_id.get(&barrier_id).expect("invalid barrier id");
current_pos.replace(Some(pos));
// client entered => send event
event_tx.send((pos, CaptureEvent::Begin)).await.expect("no channel");
tokio::select! {
_ = notify_release.notified() => { /* capture release */
log::debug!("release session requested");
},
_ = release_session.notified() => { /* release session */
log::debug!("ei devices changed");
ei_devices_changed = true;
},
_ = cancel_session.cancelled() => { /* kill session notify */
log::debug!("session cancel requested");
break
},
}
release_capture(input_capture, session, activated, pos).await?;
}
_ = notify_release.notified() => { /* capture release -> we are not capturing anyway, so ignore */
log::debug!("release session requested");
},
_ = release_session.notified() => { /* release session */
log::debug!("ei devices changed");
ei_devices_changed = true;
},
_ = cancel_session.cancelled() => { /* kill session notify */
log::debug!("session cancel requested");
break
},
}
if ei_devices_changed {
/* for whatever reason, GNOME seems to kill the session
* as soon as devices are added or removed, so we need
* to cancel */
break;
}
}
// cancel libei task
log::debug!("session exited: killing libei task");
cancel_ei_handler.cancel();
Ok::<(), CaptureError>(())
};
let (a, b) = tokio::join!(ei_task, capture_session_task);
cancel_update.cancel();
log::debug!("both session and ei task finished!");
a?;
b?;
Ok(())
}
async fn release_capture<'a>(
input_capture: &InputCapture<'a>,
session: &Session<'a, InputCapture<'a>>,
activated: Activated,
current_pos: Position,
) -> Result<(), CaptureError> {
if let Some(activation_id) = activated.activation_id() {
log::debug!("releasing input capture {activation_id}");
}
let (x, y) = activated
.cursor_position()
.expect("compositor did not report cursor position!");
log::debug!("client entered @ ({x}, {y})");
let (dx, dy) = match current_pos {
// offset cursor position to not enter again immediately
Position::Left => (1., 0.),
Position::Right => (-1., 0.),
Position::Top => (0., 1.),
Position::Bottom => (0., -1.),
};
// release 1px to the right of the entered zone
let cursor_position = (x as f64 + dx, y as f64 + dy);
input_capture
.release(session, activated.activation_id(), Some(cursor_position))
.await?;
Ok(())
}
fn find_corresponding_client(barriers: &[ICBarrier], pos: (f32, f32)) -> BarrierID {
barriers
.iter()
.copied()
.min_by_key(|b| {
let (x1, y1, x2, y2) = b.position;
let (x1, y1, x2, y2) = (x1 as f32, y1 as f32, x2 as f32, y2 as f32);
distance_to_line(((x1, y1), (x2, y2)), pos) as i32
})
.expect("could not find barrier corresponding to client")
.barrier_id
}
fn distance_to_line(line: ((f32, f32), (f32, f32)), p: (f32, f32)) -> f32 {
let ((x1, y1), (x2, y2)) = line;
let (x0, y0) = p;
/*
* we use the fact that for the triangle spanned by the line and p,
* the height of the triangle is the desired distance and can be calculated by
* h = 2A / b with b being the line_length and
*/
let double_triangle_area = ((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1).abs();
let line_length = ((y2 - y1).powf(2.0) + (x2 - x1).powf(2.0)).sqrt();
let distance = double_triangle_area / line_length;
log::debug!("distance to line({line:?}, {p:?}) = {distance}");
distance
}
static ALL_CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
async fn handle_ei_event(
ei_event: EiEvent,
current_client: Option<Position>,
context: &ei::Context,
event_tx: &Sender<(Position, CaptureEvent)>,
release_session: &Notify,
) -> Result<(), CaptureError> {
match ei_event {
EiEvent::SeatAdded(s) => {
s.seat.bind_capabilities(ALL_CAPABILITIES);
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
}
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
EiEvent::Disconnected(d) => {
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
}
_ => {
if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) {
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
}
}
}
}
Ok(())
}
#[async_trait]
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self
.notify_capture
.send(LibeiNotifyEvent::Create(pos))
.await;
Ok(())
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self
.notify_capture
.send(LibeiNotifyEvent::Destroy(pos))
.await;
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
self.notify_release.notify_waiters();
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
self.cancellation_token.cancel();
let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate...");
let res = task.await.expect("libei task panic");
log::debug!("done!");
self.terminated = true;
res
}
}
impl<'a> Drop for LibeiInputCapture<'a> {
fn drop(&mut self) {
if !self.terminated {
/* this workaround is needed until async drop is stabilized */
panic!("LibeiInputCapture dropped without being terminated!");
}
}
}
impl<'a> Stream for LibeiInputCapture<'a> {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.capture_task.poll_unpin(cx) {
Poll::Ready(r) => match r.expect("failed to join") {
Ok(()) => Poll::Ready(None),
Err(e) => Poll::Ready(Some(Err(e))),
},
Poll::Pending => self.event_rx.poll_recv(cx).map(|e| e.map(Result::Ok)),
}
}
}

View File

@@ -1,618 +0,0 @@
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
use async_trait::async_trait;
use bitflags::bitflags;
use core_foundation::base::{kCFAllocatorDefault, CFRelease};
use core_foundation::date::CFTimeInterval;
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
use core_graphics::base::{kCGErrorSuccess, CGError};
use core_graphics::display::{CGDisplay, CGPoint};
use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType, EventField,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use futures_core::Stream;
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
use keycode::{KeyMap, KeyMapping};
use libc::c_void;
use once_cell::unsync::Lazy;
use std::collections::HashSet;
use std::ffi::{c_char, CString};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{ready, Context, Poll};
use std::thread::{self};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::{oneshot, Mutex};
#[derive(Debug, Default)]
struct Bounds {
xmin: f64,
xmax: f64,
ymin: f64,
ymax: f64,
}
#[derive(Debug)]
struct InputCaptureState {
active_clients: Lazy<HashSet<Position>>,
current_pos: Option<Position>,
bounds: Bounds,
}
#[derive(Debug)]
enum ProducerEvent {
Release,
Create(Position),
Destroy(Position),
Grab(Position),
EventTapDisabled,
}
impl InputCaptureState {
fn new() -> Result<Self, MacosCaptureCreationError> {
let mut res = Self {
active_clients: Lazy::new(HashSet::new),
current_pos: None,
bounds: Bounds::default(),
};
res.update_bounds()?;
Ok(res)
}
fn crossed(&mut self, event: &CGEvent) -> Option<Position> {
let location = event.location();
let relative_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X);
let relative_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y);
for &position in self.active_clients.iter() {
if (position == Position::Left && (location.x + relative_x) <= self.bounds.xmin)
|| (position == Position::Right && (location.x + relative_x) >= self.bounds.xmax)
|| (position == Position::Top && (location.y + relative_y) <= self.bounds.ymin)
|| (position == Position::Bottom && (location.y + relative_y) >= self.bounds.ymax)
{
log::debug!("Crossed barrier into position: {position:?}");
return Some(position);
}
}
None
}
// Get the max bounds of all displays
fn update_bounds(&mut self) -> Result<(), MacosCaptureCreationError> {
let active_ids =
CGDisplay::active_displays().map_err(MacosCaptureCreationError::ActiveDisplays)?;
active_ids.iter().for_each(|d| {
let bounds = CGDisplay::new(*d).bounds();
self.bounds.xmin = self.bounds.xmin.min(bounds.origin.x);
self.bounds.xmax = self.bounds.xmax.max(bounds.origin.x + bounds.size.width);
self.bounds.ymin = self.bounds.ymin.min(bounds.origin.y);
self.bounds.ymax = self.bounds.ymax.max(bounds.origin.y + bounds.size.height);
});
log::debug!("Updated displays bounds: {0:?}", self.bounds);
Ok(())
}
// We can't disable mouse movement when in a client so we need to reset the cursor position
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
// random location when we exit the client
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
if let Some(pos) = self.current_pos {
let location = event.location();
let edge_offset = 1.0;
// After the cursor is warped no event is produced but the next event
// will carry the delta from the warp so only half the delta is needed to move the cursor
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
let mut new_x = location.x + delta_x;
let mut new_y = location.y + delta_y;
match pos {
Position::Left => {
new_x = self.bounds.xmin + edge_offset;
}
Position::Right => {
new_x = self.bounds.xmax - edge_offset;
}
Position::Top => {
new_y = self.bounds.ymin + edge_offset;
}
Position::Bottom => {
new_y = self.bounds.ymax - edge_offset;
}
}
let new_pos = CGPoint::new(new_x, new_y);
log::trace!("Resetting cursor position to: {new_x}, {new_y}");
return CGDisplay::warp_mouse_cursor_position(new_pos)
.map_err(CaptureError::WarpCursor);
}
Err(CaptureError::ResetMouseWithoutClient)
}
async fn handle_producer_event(
&mut self,
producer_event: ProducerEvent,
) -> Result<(), CaptureError> {
log::debug!("handling event: {producer_event:?}");
match producer_event {
ProducerEvent::Release => {
if self.current_pos.is_some() {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None;
}
}
ProducerEvent::Grab(pos) => {
if self.current_pos.is_none() {
CGDisplay::hide_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = Some(pos);
}
}
ProducerEvent::Create(p) => {
self.active_clients.insert(p);
}
ProducerEvent::Destroy(p) => {
if let Some(current) = self.current_pos {
if current == p {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None;
};
}
self.active_clients.remove(&p);
}
ProducerEvent::EventTapDisabled => return Err(CaptureError::EventTapDisabled),
};
Ok(())
}
}
fn get_events(
ev_type: &CGEventType,
ev: &CGEvent,
result: &mut Vec<CaptureEvent>,
) -> Result<(), CaptureError> {
fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
PointerEvent::Motion {
time: 0,
dx: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X),
dy: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y),
}
}
fn map_key(ev: &CGEvent) -> Result<u32, CaptureError> {
let code = ev.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
match KeyMap::from_key_mapping(KeyMapping::Mac(code as u16)) {
Ok(k) => Ok(k.evdev as u32),
Err(()) => Err(CaptureError::KeyMapError(code)),
}
}
match ev_type {
CGEventType::KeyDown => {
let k = map_key(ev)?;
result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key: k,
state: 1,
})));
}
CGEventType::KeyUp => {
let k = map_key(ev)?;
result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key: k,
state: 0,
})));
}
CGEventType::FlagsChanged => {
let mut mods = XMods::empty();
let mut mods_locked = XMods::empty();
let cg_flags = ev.get_flags();
if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
mods |= XMods::ShiftMask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
mods |= XMods::ControlMask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
mods |= XMods::Mod1Mask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
mods |= XMods::Mod4Mask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
mods |= XMods::LockMask;
mods_locked |= XMods::LockMask;
}
let modifier_event = KeyboardEvent::Modifiers {
depressed: mods.bits(),
latched: 0,
locked: mods_locked.bits(),
group: 0,
};
result.push(CaptureEvent::Input(Event::Keyboard(modifier_event)));
}
CGEventType::MouseMoved => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::LeftMouseDragged => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::RightMouseDragged => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::OtherMouseDragged => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::LeftMouseDown => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
})))
}
CGEventType::LeftMouseUp => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
})))
}
CGEventType::RightMouseDown => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
})))
}
CGEventType::RightMouseUp => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
})))
}
CGEventType::OtherMouseDown => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
})))
}
CGEventType::OtherMouseUp => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
})))
}
CGEventType::ScrollWheel => {
let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1);
let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2);
if v != 0 {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 0, // Vertical
value: v as f64,
})));
}
if h != 0 {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 1, // Horizontal
value: h as f64,
})));
}
}
_ => (),
}
Ok(())
}
fn create_event_tap<'a>(
client_state: Arc<Mutex<InputCaptureState>>,
notify_tx: Sender<ProducerEvent>,
event_tx: Sender<(Position, CaptureEvent)>,
) -> Result<CGEventTap<'a>, MacosCaptureCreationError> {
let cg_events_of_interest: Vec<CGEventType> = vec![
CGEventType::LeftMouseDown,
CGEventType::LeftMouseUp,
CGEventType::RightMouseDown,
CGEventType::RightMouseUp,
CGEventType::OtherMouseDown,
CGEventType::OtherMouseUp,
CGEventType::MouseMoved,
CGEventType::LeftMouseDragged,
CGEventType::RightMouseDragged,
CGEventType::OtherMouseDragged,
CGEventType::ScrollWheel,
CGEventType::KeyDown,
CGEventType::KeyUp,
CGEventType::FlagsChanged,
];
let event_tap_callback =
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
log::trace!("Got event from tap: {event_type:?}");
let mut state = client_state.blocking_lock();
let mut pos = None;
let mut res_events = vec![];
if matches!(
event_type,
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput
) {
log::error!("CGEventTap disabled");
notify_tx
.blocking_send(ProducerEvent::EventTapDisabled)
.unwrap_or_else(|e| {
log::error!("Failed to send notification: {e}");
});
}
// Are we in a client?
if let Some(current_pos) = state.current_pos {
pos = Some(current_pos);
get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| {
log::error!("Failed to get events: {e}");
});
// Keep (hidden) cursor at the edge of the screen
if matches!(event_type, CGEventType::MouseMoved) {
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
log::error!("Failed to reset mouse position: {e}");
})
}
}
// Did we cross a barrier?
else if matches!(event_type, CGEventType::MouseMoved) {
if let Some(new_pos) = state.crossed(cg_ev) {
pos = Some(new_pos);
res_events.push(CaptureEvent::Begin);
notify_tx
.blocking_send(ProducerEvent::Grab(new_pos))
.expect("Failed to send notification");
}
}
if let Some(pos) = pos {
res_events.iter().for_each(|e| {
event_tx
.blocking_send((pos, *e))
.expect("Failed to send event");
});
// Returning None should stop the event from being processed
// but core fundation still returns the event
cg_ev.set_type(CGEventType::Null);
}
Some(cg_ev.to_owned())
};
let tap = CGEventTap::new(
CGEventTapLocation::Session,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
cg_events_of_interest,
event_tap_callback,
)
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap
.mach_port
.create_runloop_source(0)
.expect("Failed creating loop source");
unsafe {
CFRunLoop::get_current().add_source(&tap_source, kCFRunLoopCommonModes);
}
Ok(tap)
}
fn event_tap_thread(
client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>,
exit: oneshot::Sender<Result<(), &'static str>>,
) {
let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
Err(e) => {
ready.send(Err(e)).expect("channel closed");
return;
}
Ok(tap) => {
ready.send(Ok(())).expect("channel closed");
tap
}
};
CFRunLoop::run_current();
let _ = exit.send(Err("tap thread exited"));
}
pub struct MacOSInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
}
impl MacOSInputCapture {
pub async fn new() -> Result<Self, MacosCaptureCreationError> {
let state = Arc::new(Mutex::new(InputCaptureState::new()?));
let (event_tx, event_rx) = mpsc::channel(32);
let (notify_tx, mut notify_rx) = mpsc::channel(32);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (tap_exit_tx, mut tap_exit_rx) = oneshot::channel();
unsafe {
configure_cf_settings()?;
}
log::info!("Enabling CGEvent tap");
let event_tap_thread_state = state.clone();
let event_tap_notify = notify_tx.clone();
thread::spawn(move || {
event_tap_thread(
event_tap_thread_state,
event_tx,
event_tap_notify,
ready_tx,
tap_exit_tx,
)
});
// wait for event tap creation result
ready_rx.recv().expect("channel closed")?;
let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
loop {
tokio::select! {
producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed");
let mut state = state.lock().await;
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
log::error!("Failed to handle producer event: {e}");
})
}
res = &mut tap_exit_rx => {
if let Err(e) = res.expect("channel closed") {
log::error!("Tap thread failed: {:?}", e);
break;
}
}
}
}
});
Ok(Self {
event_rx,
notify_tx,
})
}
}
#[async_trait]
impl Capture for MacOSInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("creating capture, {pos}");
let _ = notify_tx.send(ProducerEvent::Create(pos)).await;
log::debug!("done !");
});
Ok(())
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("destroying capture {pos}");
let _ = notify_tx.send(ProducerEvent::Destroy(pos)).await;
log::debug!("done !");
});
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("notifying Release");
let _ = notify_tx.send(ProducerEvent::Release).await;
});
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
impl Stream for MacOSInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None),
Some(e) => Poll::Ready(Some(Ok(e))),
}
}
}
type CGSConnectionID = u32;
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn CGSSetConnectionProperty(
cid: CGSConnectionID,
targetCID: CGSConnectionID,
key: CFStringRef,
value: CFBooleanRef,
) -> CGError;
fn _CGSDefaultConnection() -> CGSConnectionID;
}
extern "C" {
fn CGEventSourceSetLocalEventsSuppressionInterval(
event_source: CGEventSource,
seconds: CFTimeInterval,
);
}
unsafe fn configure_cf_settings() -> Result<(), MacosCaptureCreationError> {
// When we warp the cursor using CGWarpMouseCursorPosition local events are suppressed for a short time
// this leeds to the cursor not flowing when crossing back from a clinet, set this to to 0 stops the warp
// from working, set a low value by trial and error, 0.05s seems good. 0.25s is the default
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
.map_err(|_| MacosCaptureCreationError::EventSourceCreation)?;
CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.05);
// This is a private settings that allows the cursor to be hidden while in the background.
// It is used by Barrier and other apps.
let key = CString::new("SetsCursorInBackground").unwrap();
let cf_key = CFStringCreateWithCString(
kCFAllocatorDefault,
key.as_ptr() as *const c_char,
kCFStringEncodingUTF8,
);
if CGSSetConnectionProperty(
_CGSDefaultConnection(),
_CGSDefaultConnection(),
cf_key,
kCFBooleanTrue,
) != kCGErrorSuccess
{
return Err(MacosCaptureCreationError::CGCursorProperty);
}
CFRelease(cf_key as *const c_void);
Ok(())
}
// From X11/X.h
bitflags! {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct XMods: u32 {
const ShiftMask = (1<<0);
const LockMask = (1<<1);
const ControlMask = (1<<2);
const Mod1Mask = (1<<3);
const Mod2Mask = (1<<4);
const Mod3Mask = (1<<5);
const Mod4Mask = (1<<6);
const Mod5Mask = (1<<7);
}
}

View File

@@ -1,44 +0,0 @@
use std::task::Poll;
use async_trait::async_trait;
use futures_core::Stream;
use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
pub struct X11InputCapture {}
impl X11InputCapture {
pub fn new() -> std::result::Result<Self, X11InputCaptureCreationError> {
Err(X11InputCaptureCreationError::NotImplemented)
}
}
#[async_trait]
impl Capture for X11InputCapture {
async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
impl Stream for X11InputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
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,72 +0,0 @@
[package]
name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
async-trait = "0.1.80"
futures = "0.3.28"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.3.0" }
thiserror = "2.0.0"
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.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0"
core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "0.4.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[features]
default = ["wlroots", "x11", "remote_desktop_portal", "libei"]
wlroots = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",
"dep:wayland-protocols-misc",
]
x11 = ["dep:x11"]
remote_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -1,32 +0,0 @@
use async_trait::async_trait;
use input_event::Event;
use crate::error::EmulationError;
use super::{Emulation, EmulationHandle};
#[derive(Default)]
pub(crate) struct DummyEmulation;
impl DummyEmulation {
pub(crate) fn new() -> Self {
Self {}
}
}
#[async_trait]
impl Emulation 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) {}
async fn terminate(&mut self) {
/* nothing to do */
}
}

View File

@@ -1,161 +0,0 @@
#[derive(Debug, Error)]
pub enum InputEmulationError {
#[error("error creating input-emulation: `{0}`")]
Create(#[from] EmulationCreationError),
#[error("error emulating input: `{0}`")]
Emulate(#[from] EmulationError),
}
#[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
use ashpd::{desktop::ResponseError, Error::Response};
use std::io;
use thiserror::Error;
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[derive(Debug, Error)]
pub enum EmulationError {
#[error("event stream closed")]
EndOfStream,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error: `{0}`")]
Libei(#[from] reis::Error),
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(all(
unix,
any(feature = "remote_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 = "wlroots", 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 = "remote_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,
}
impl EmulationCreationError {
/// request was intentionally denied by the user
pub(crate) fn cancelled_by_user(&self) -> bool {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
if matches!(
self,
EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response(
ResponseError::Cancelled,
)))
) {
return true;
}
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
if matches!(
self,
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
ResponseError::Cancelled,
)))
) {
return true;
}
false
}
}
#[cfg(all(unix, feature = "wlroots", 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 = "wlroots", 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 = "wlroots", 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)]
Reis(#[from] reis::Error),
}
#[cfg(all(unix, feature = "remote_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 {}

View File

@@ -1,240 +0,0 @@
use async_trait::async_trait;
use std::{
collections::{HashMap, HashSet},
fmt::Display,
};
use input_event::{Event, KeyboardEvent};
pub use self::error::{EmulationCreationError, EmulationError, InputEmulationError};
#[cfg(windows)]
mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11;
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
mod wlroots;
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei;
#[cfg(target_os = "macos")]
mod macos;
/// fallback input emulation (logs events)
mod dummy;
mod error;
pub type EmulationHandle = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei,
#[cfg(all(unix, feature = "remote_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 = "wlroots", 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 = "remote_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"),
}
}
}
pub struct InputEmulation {
emulation: Box<dyn Emulation>,
handles: HashSet<EmulationHandle>,
pressed_keys: HashMap<EmulationHandle, HashSet<u32>>,
}
impl InputEmulation {
async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
let emulation: Box<dyn Emulation> = match backend {
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Box::new(x11::X11Emulation::new()?),
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
#[cfg(windows)]
Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
#[cfg(target_os = "macos")]
Backend::MacOs => Box::new(macos::MacOSEmulation::new()?),
Backend::Dummy => Box::new(dummy::DummyEmulation::new()),
};
Ok(Self {
emulation,
handles: HashSet::new(),
pressed_keys: HashMap::new(),
})
}
pub async fn new(backend: Option<Backend>) -> Result<InputEmulation, EmulationCreationError> {
if let Some(backend) = backend {
let b = Self::with_backend(backend).await;
if b.is_ok() {
log::info!("using emulation backend: {backend}");
}
return b;
}
for backend in [
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei,
#[cfg(all(unix, feature = "remote_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 Self::with_backend(backend).await {
Ok(b) => {
log::info!("using emulation backend: {backend}");
return Ok(b);
}
Err(e) if e.cancelled_by_user() => return Err(e),
Err(e) => log::warn!("{e}"),
}
}
Err(EmulationCreationError::NoAvailableBackend)
}
pub async fn consume(
&mut self,
event: Event,
handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Event::Keyboard(KeyboardEvent::Key { key, state, .. }) => {
// prevent double pressed / released keys
if self.update_pressed_keys(handle, key, state) {
self.emulation.consume(event, handle).await?;
}
Ok(())
}
_ => self.emulation.consume(event, handle).await,
}
}
pub async fn create(&mut self, handle: EmulationHandle) -> bool {
if self.handles.insert(handle) {
self.pressed_keys.insert(handle, HashSet::new());
self.emulation.create(handle).await;
true
} else {
false
}
}
pub async fn destroy(&mut self, handle: EmulationHandle) {
let _ = self.release_keys(handle).await;
if self.handles.remove(&handle) {
self.pressed_keys.remove(&handle);
self.emulation.destroy(handle).await
}
}
pub async fn terminate(&mut self) {
for handle in self.handles.iter().cloned().collect::<Vec<_>>() {
self.destroy(handle).await
}
self.emulation.terminate().await
}
pub async fn release_keys(&mut self, handle: EmulationHandle) -> Result<(), EmulationError> {
if let Some(keys) = self.pressed_keys.get_mut(&handle) {
let keys = keys.drain().collect::<Vec<_>>();
for key in keys {
let event = Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state: 0,
});
self.emulation.consume(event, handle).await?;
if let Ok(key) = input_event::scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}");
}
}
}
let event = Event::Keyboard(KeyboardEvent::Modifiers {
depressed: 0,
latched: 0,
locked: 0,
group: 0,
});
self.emulation.consume(event, handle).await?;
Ok(())
}
pub fn has_pressed_keys(&self, handle: EmulationHandle) -> bool {
self.pressed_keys
.get(&handle)
.is_some_and(|p| !p.is_empty())
}
/// update the pressed_keys for the given handle
/// returns whether the event should be processed
fn update_pressed_keys(&mut self, handle: EmulationHandle, key: u32, state: u8) -> bool {
let Some(pressed_keys) = self.pressed_keys.get_mut(&handle) else {
return false;
};
if state == 0 {
// currently pressed => can release
pressed_keys.remove(&key)
} else {
// currently not pressed => can press
pressed_keys.insert(key)
}
}
}
#[async_trait]
trait Emulation: 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);
async fn terminate(&mut self);
}

View File

@@ -1,336 +0,0 @@
use futures::{future, StreamExt};
use std::{
io,
os::{fd::OwnedFd, unix::net::UnixStream},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::{SystemTime, UNIX_EPOCH},
};
use tokio::task::JoinHandle;
use ashpd::desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session,
};
use async_trait::async_trait;
use reis::{
ei::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
Pointer, Scroll,
},
event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::EiConvertEventStream,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::EmulationError;
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
#[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(crate) struct LibeiEmulation<'a> {
context: ei::Context,
conn: event::Connection,
devices: Devices,
ei_task: JoinHandle<()>,
error: Arc<Mutex<Option<EmulationError>>>,
libei_error: Arc<AtomicBool>,
_remote_desktop: RemoteDesktop<'a>,
session: Session<'a, RemoteDesktop<'a>>,
}
async fn get_ei_fd<'a>(
) -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
let remote_desktop = RemoteDesktop::new().await?;
log::debug!("creating session ...");
let session = remote_desktop.create_session().await?;
log::debug!("selecting devices ...");
remote_desktop
.select_devices(
&session,
DeviceType::Keyboard | DeviceType::Pointer,
None,
PersistMode::ExplicitlyRevoked,
)
.await?;
log::info!("requesting permission for input emulation");
let _devices = remote_desktop.start(&session, None).await?.response()?;
let fd = remote_desktop.connect_to_eis(&session).await?;
Ok((remote_desktop, session, fd))
}
impl<'a> LibeiEmulation<'a> {
pub(crate) async fn new() -> Result<Self, LibeiEmulationCreationError> {
let (_remote_desktop, session, eifd) = get_ei_fd().await?;
let stream = UnixStream::from(eifd);
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
let (conn, events) = context
.handshake_tokio("de.feschber.LanMouse", ContextType::Sender)
.await?;
let devices = Devices::default();
let libei_error = Arc::new(AtomicBool::default());
let error = Arc::new(Mutex::new(None));
let ei_handler = ei_task(
events,
conn.clone(),
context.clone(),
devices.clone(),
libei_error.clone(),
error.clone(),
);
let ei_task = tokio::task::spawn_local(ei_handler);
Ok(Self {
context,
conn,
devices,
ei_task,
error,
libei_error,
_remote_desktop,
session,
})
}
}
impl<'a> Drop for LibeiEmulation<'a> {
fn drop(&mut self) {
self.ei_task.abort();
}
}
#[async_trait]
impl<'a> Emulation for LibeiEmulation<'a> {
async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
if self.libei_error.load(Ordering::SeqCst) {
// don't break sending additional events but signal error
if let Some(e) = self.error.lock().unwrap().take() {
return Err(e);
}
}
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.conn.serial(), 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.conn.serial(), 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.conn.serial(), 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.conn.serial(), now);
}
}
},
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.conn.serial(), 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 terminate(&mut self) {
let _ = self.session.close().await;
self.ei_task.abort();
}
}
async fn ei_task(
mut events: EiConvertEventStream,
_conn: Connection,
context: ei::Context,
devices: Devices,
libei_error: Arc<AtomicBool>,
error: Arc<Mutex<Option<EmulationError>>>,
) {
loop {
match ei_event_handler(&mut events, &context, &devices).await {
Ok(()) => {}
Err(e) => {
libei_error.store(true, Ordering::SeqCst);
error.lock().unwrap().replace(e);
// wait for termination -> otherwise we will loop forever
future::pending::<()>().await;
}
}
}
}
async fn ei_event_handler(
events: &mut EiConvertEventStream,
context: &ei::Context,
devices: &Devices,
) -> Result<(), EmulationError> {
loop {
let event = events.next().await.ok_or(EmulationError::EndOfStream)??;
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:?}");
return Err(EmulationError::EndOfStream);
}
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))?;
}
}

View File

@@ -1,495 +0,0 @@
use super::{error::EmulationError, Emulation, EmulationHandle};
use async_trait::async_trait;
use bitflags::bitflags;
use core_graphics::base::CGFloat;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayBounds, CGGetDisplaysWithRect, CGPoint, CGRect, CGSize,
};
use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField,
ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping};
use std::cell::Cell;
use std::ops::{Index, IndexMut};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use tokio::{sync::Notify, task::JoinHandle};
use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub(crate) struct MacOSEmulation {
event_source: CGEventSource,
repeat_task: Option<JoinHandle<()>>,
button_state: ButtonState,
modifier_state: Rc<Cell<XMods>>,
notify_repeat_task: Arc<Notify>,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSEmulation {}
impl MacOSEmulation {
pub(crate) fn new() -> Result<Self, MacOSEmulationCreationError> {
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
.map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?;
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
repeat_task: None,
notify_repeat_task: Arc::new(Notify::new()),
modifier_state: Rc::new(Cell::new(XMods::empty())),
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's
// always the last to be pressed
self.cancel_repeat_task().await;
let event_source = self.event_source.clone();
let notify = self.notify_repeat_task.clone();
let modifiers = self.modifier_state.clone();
let repeat_task = tokio::task::spawn_local(async move {
let stop = tokio::select! {
_ = tokio::time::sleep(DEFAULT_REPEAT_DELAY) => false,
_ = notify.notified() => true,
};
if !stop {
loop {
key_event(event_source.clone(), key, 1, modifiers.get());
tokio::select! {
_ = tokio::time::sleep(DEFAULT_REPEAT_INTERVAL) => {},
_ = notify.notified() => break,
}
}
}
// release key when cancelled
update_modifiers(&modifiers, key as u32, 0);
key_event(event_source.clone(), key, 0, modifiers.get());
});
self.repeat_task = Some(repeat_task);
}
async fn cancel_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
self.notify_repeat_task.notify_waiters();
let _ = task.await;
}
}
}
fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return;
}
};
event.set_flags(to_cgevent_flags(modifiers));
event.post(CGEventTapLocation::HID);
log::trace!("key event: {key} {state}");
}
fn modifier_event(event_source: CGEventSource, depressed: XMods) {
let Ok(event) = CGEvent::new(event_source) else {
log::warn!("could not create CGEvent");
return;
};
let flags = to_cgevent_flags(depressed);
event.set_type(CGEventType::FlagsChanged);
event.set_flags(flags);
event.post(CGEventTapLocation::HID);
log::trace!("modifiers updated: {depressed:?}");
}
fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
let mut displays: [CGDirectDisplayID; 16] = [0; 16];
let mut display_count: u32 = 0;
let rect = CGRect::new(&CGPoint::new(x, y), &CGSize::new(0.0, 0.0));
let error = unsafe {
CGGetDisplaysWithRect(
rect,
1,
displays.as_mut_ptr(),
&mut display_count as *mut u32,
)
};
if error != 0 {
log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
return Option::None;
}
if display_count == 0 {
log::debug!("no displays found at point ({}, {})", x, y);
return Option::None;
}
return displays.first().copied();
}
fn get_display_bounds(display: CGDirectDisplayID) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
unsafe {
let bounds = CGDisplayBounds(display);
let min_x = bounds.origin.x;
let max_x = bounds.origin.x + bounds.size.width;
let min_y = bounds.origin.y;
let max_y = bounds.origin.y + bounds.size.height;
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
}
}
fn clamp_to_screen_space(
current_x: CGFloat,
current_y: CGFloat,
dx: CGFloat,
dy: CGFloat,
) -> (CGFloat, CGFloat) {
// Check which display the mouse is currently on
// Determine what the location of the mouse would be after applying the move
// Get the display at the new location
// If the point is not on a display
// Clamp the mouse to the current display
// Else If the point is on a display
// Clamp the mouse to the new display
let current_display = match get_display_at_point(current_x, current_y) {
Some(display) => display,
None => {
log::warn!("could not get current display!");
return (current_x, current_y);
}
};
let new_x = current_x + dx;
let new_y = current_y + dy;
let final_display = get_display_at_point(new_x, new_y).unwrap_or(current_display);
let (min_x, min_y, max_x, max_y) = get_display_bounds(final_display);
(
new_x.clamp(min_x, max_x - 1.),
new_y.clamp(min_y, max_y - 1.),
)
}
#[async_trait]
impl Emulation for MacOSEmulation {
async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return Ok(());
}
};
let (new_mouse_x, new_mouse_y) =
clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy);
mouse_location.x = new_mouse_x;
mouse_location.y = new_mouse_y;
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return Ok(());
}
};
// store button state
self.button_state[mouse_button] = state == 1;
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
Ok(k) => k.mac as CGKeyCode,
Err(_) => {
log::warn!("unable to map key event");
return Ok(());
}
};
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.cancel_repeat_task().await,
}
update_modifiers(&self.modifier_state, key, state);
key_event(
self.event_source.clone(),
code,
state,
self.modifier_state.get(),
);
}
KeyboardEvent::Modifiers {
depressed,
latched,
locked,
group,
} => {
set_modifiers(&self.modifier_state, depressed, latched, locked, group);
modifier_event(self.event_source.clone(), self.modifier_state.get());
}
},
}
// FIXME
Ok(())
}
async fn create(&mut self, _handle: EmulationHandle) {}
async fn destroy(&mut self, _handle: EmulationHandle) {}
async fn terminate(&mut self) {}
}
fn update_modifiers(modifiers: &Cell<XMods>, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) {
let mask = match key {
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
scancode::Linux::KeyCapsLock => XMods::LockMask,
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
scancode::Linux::KeyLeftAlt | scancode::Linux::KeyRightalt => XMods::Mod1Mask,
scancode::Linux::KeyLeftMeta | scancode::Linux::KeyRightmeta => XMods::Mod4Mask,
_ => XMods::empty(),
};
// unchanged
if mask.is_empty() {
return false;
}
let mut mods = modifiers.get();
match state {
1 => mods.insert(mask),
_ => mods.remove(mask),
}
modifiers.set(mods);
true
} else {
false
}
}
fn set_modifiers(
active_modifiers: &Cell<XMods>,
depressed: u32,
latched: u32,
locked: u32,
group: u32,
) {
let depressed = XMods::from_bits(depressed).unwrap_or_default();
let _latched = XMods::from_bits(latched).unwrap_or_default();
let _locked = XMods::from_bits(locked).unwrap_or_default();
let _group = XMods::from_bits(group).unwrap_or_default();
// we only care about the depressed modifiers for now
active_modifiers.replace(depressed);
}
fn to_cgevent_flags(depressed: XMods) -> CGEventFlags {
let mut flags = CGEventFlags::empty();
if depressed.contains(XMods::ShiftMask) {
flags |= CGEventFlags::CGEventFlagShift;
}
if depressed.contains(XMods::LockMask) {
flags |= CGEventFlags::CGEventFlagAlphaShift;
}
if depressed.contains(XMods::ControlMask) {
flags |= CGEventFlags::CGEventFlagControl;
}
if depressed.contains(XMods::Mod1Mask) {
flags |= CGEventFlags::CGEventFlagAlternate;
}
if depressed.contains(XMods::Mod4Mask) {
flags |= CGEventFlags::CGEventFlagCommand;
}
flags
}
// From X11/X.h
bitflags! {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct XMods: u32 {
const ShiftMask = (1<<0);
const LockMask = (1<<1);
const ControlMask = (1<<2);
const Mod1Mask = (1<<3);
const Mod2Mask = (1<<4);
const Mod3Mask = (1<<5);
const Mod4Mask = (1<<6);
const Mod5Mask = (1<<7);
}
}

View File

@@ -1,161 +0,0 @@
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
PersistMode, Session,
},
zbus::AsyncDrop,
};
use async_trait::async_trait;
use futures::FutureExt;
use input_event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
};
use crate::error::EmulationError;
use super::{error::XdpEmulationCreationError, Emulation, EmulationHandle};
pub(crate) struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a, RemoteDesktop<'a>>,
}
impl<'a> DesktopPortalEmulation<'a> {
pub(crate) async fn new() -> Result<DesktopPortalEmulation<'a>, XdpEmulationCreationError> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(
&session,
DeviceType::Keyboard | DeviceType::Pointer,
None,
PersistMode::ExplicitlyRevoked,
)
.await?;
log::info!("requesting permission for input emulation");
let _devices = proxy.start(&session, None).await?.response()?;
log::debug!("started session");
let session = session;
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> Emulation for DesktopPortalEmulation<'a> {
async fn consume(
&mut self,
event: input_event::Event,
_client: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Pointer(p) => match p {
PointerEvent::Motion { time: _, dx, dy } => {
self.proxy
.notify_pointer_motion(&self.session, dx, dy)
.await?;
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
self.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await?;
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
self.proxy
.notify_pointer_axis_discrete(&self.session, axis, value / 120)
.await?;
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
let (dx, dy) = match axis {
Axis::Vertical => (0., value),
Axis::Horizontal => (value, 0.),
};
self.proxy
.notify_pointer_axis(&self.session, dx, dy, true)
.await?;
}
},
Keyboard(k) => {
match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
self.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await?;
}
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
}
Ok(())
}
async fn create(&mut self, _client: EmulationHandle) {}
async fn destroy(&mut self, _client: EmulationHandle) {}
async fn terminate(&mut self) {
if let Err(e) = self.session.close().await {
log::warn!("session.close(): {e}");
};
if let Err(e) = self.session.receive_closed().await {
log::warn!("session.receive_closed(): {e}");
};
}
}
impl<'a> AsyncDrop for DesktopPortalEmulation<'a> {
#[doc = r" Perform the async cleanup."]
#[must_use]
#[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()
}
}

View File

@@ -1,21 +0,0 @@
[package]
name = "input-event"
description = "cross-platform input-event types for input-capture / input-emulation"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures-core = "0.3.30"
log = "0.4.22"
num_enum = "0.7.2"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
reis = { version = "0.4", optional = true }
[features]
default = ["libei"]
libei = ["dep:reis"]

View File

@@ -1 +0,0 @@

View File

@@ -1,119 +0,0 @@
use std::fmt::{self, Display};
pub mod error;
pub mod scancode;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei;
// 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 },
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum KeyboardEvent {
/// a key press / release event
Key { time: u32, key: u32, state: u8 },
/// modifiers changed state
Modifiers {
depressed: u32,
latched: u32,
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),
}
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})")
}
}
}
}
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 {
depressed: mods_depressed,
latched: mods_latched,
locked: 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),
}
}
}

View File

@@ -1,146 +0,0 @@
use reis::{
ei::{button::ButtonState, keyboard::KeyState},
event::EiEvent,
};
use crate::{Event, KeyboardEvent, PointerEvent};
impl Event {
pub fn from_ei_event(ei_event: EiEvent) -> impl Iterator<Item = Self> {
to_input_events(ei_event).into_iter()
}
}
enum Events {
None,
One(Event),
Two(Event, Event),
}
impl Events {
fn into_iter(self) -> impl Iterator<Item = Event> {
EventIterator::new(self)
}
}
struct EventIterator {
events: [Option<Event>; 2],
pos: usize,
}
impl EventIterator {
fn new(events: Events) -> Self {
let events = match events {
Events::None => [None, None],
Events::One(e) => [Some(e), None],
Events::Two(e, f) => [Some(e), Some(f)],
};
Self { events, pos: 0 }
}
}
impl Iterator for EventIterator {
type Item = Event;
fn next(&mut self) -> Option<Self::Item> {
let res = if self.pos >= self.events.len() {
None
} else {
self.events[self.pos]
};
self.pos += 1;
res
}
}
fn to_input_events(ei_event: EiEvent) -> Events {
match ei_event {
EiEvent::KeyboardModifiers(mods) => {
let modifier_event = KeyboardEvent::Modifiers {
depressed: mods.depressed,
latched: mods.latched,
locked: mods.locked,
group: mods.group,
};
Events::One(Event::Keyboard(modifier_event))
}
EiEvent::Frame(_) => Events::None, /* FIXME */
EiEvent::PointerMotion(motion) => {
let motion_event = PointerEvent::Motion {
time: motion.time as u32,
dx: motion.dx as f64,
dy: motion.dy as f64,
};
Events::One(Event::Pointer(motion_event))
}
EiEvent::PointerMotionAbsolute(_) => Events::None,
EiEvent::Button(button) => {
let button_event = PointerEvent::Button {
time: button.time as u32,
button: button.button,
state: match button.state {
ButtonState::Released => 0,
ButtonState::Press => 1,
},
};
Events::One(Event::Pointer(button_event))
}
EiEvent::ScrollDelta(delta) => {
let dy = Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 0,
value: delta.dy as f64,
});
let dx = Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 1,
value: delta.dx as f64,
});
if delta.dy != 0. && delta.dx != 0. {
Events::Two(dy, dx)
} else if delta.dy != 0. {
Events::One(dy)
} else if delta.dx != 0. {
Events::One(dx)
} else {
Events::None
}
}
EiEvent::ScrollStop(_) => Events::None, /* TODO */
EiEvent::ScrollCancel(_) => Events::None, /* TODO */
EiEvent::ScrollDiscrete(scroll) => {
let dy = Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: 0,
value: scroll.discrete_dy,
});
let dx = Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: 1,
value: scroll.discrete_dx,
});
if scroll.discrete_dy != 0 && scroll.discrete_dx != 0 {
Events::Two(dy, dx)
} else if scroll.discrete_dy != 0 {
Events::One(dy)
} else if scroll.discrete_dx != 0 {
Events::One(dx)
} else {
Events::None
}
}
EiEvent::KeyboardKey(key) => {
let key_event = KeyboardEvent::Key {
key: key.key,
state: match key.state {
KeyState::Press => 1,
KeyState::Released => 0,
},
time: key.time as u32,
};
Events::One(Event::Keyboard(key_event))
}
EiEvent::TouchDown(_) => Events::None, /* TODO */
EiEvent::TouchUp(_) => Events::None, /* TODO */
EiEvent::TouchMotion(_) => Events::None, /* TODO */
_ => Events::None,
}
}

View File

@@ -1,18 +0,0 @@
[package]
name = "lan-mouse-cli"
description = "CLI Frontend for lan-mouse"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"rt",
] }

View File

@@ -1,18 +0,0 @@
[package]
name = "lan-mouse-gtk"
description = "GTK4 / Libadwaita Frontend for lan-mouse"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] }
adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
async-channel = { version = "2.1.1" }
hostname = "0.4.0"
log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
[build-dependencies]
glib-build-tools = { version = "0.20.0" }

View File

@@ -1,8 +0,0 @@
fn main() {
// composite_templates
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresource.xml",
"lan-mouse.gresource",
);
}

View File

@@ -1,227 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<menu id="main-menu">
<item>
<attribute name="label" translatable="yes">_Close window</attribute>
<attribute name="action">window.close</attribute>
</item>
</menu>
<template class="LanMouseWindow" parent="AdwApplicationWindow">
<property name="width-request">600</property>
<property name="height-request">700</property>
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child type="top">
<object class="AdwHeaderBar">
<child type ="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">de.feschber.LanMouse</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">600</property>
<property name="tightening-threshold">0</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup" id="capture_emulation_group">
<property name="title" translatable="yes">Capture / Emulation Status</property>
<child>
<object class="AdwActionRow" id="capture_status_row">
<property name="title">input capture is disabled</property>
<property name="subtitle">required for outgoing and incoming connections</property>
<property name="icon-name">dialog-warning-symbolic</property>
<child>
<object class="GtkButton" id="input_capture_button">
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="label" translatable="yes">Reenable</property>
</object>
</property>
<signal name="clicked" handler="handle_capture" swapped="true"/>
<property name="valign">center</property>
<style>
<class name="circular"/>
<class name="flat"/>
</style>
</object>
</child>
<style>
<class name="warning"/>
</style>
</object>
</child>
<child>
<object class="AdwActionRow" id="emulation_status_row">
<property name="title">input emulation is disabled</property>
<property name="subtitle">required for incoming connections</property>
<property name="icon-name">dialog-warning-symbolic</property>
<child>
<object class="GtkButton" id="input_emulation_button">
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="label" translatable="yes">Reenable</property>
</object>
</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_emulation" swapped="true"/>
<style>
<class name="circular"/>
<class name="flat"/>
</style>
</object>
</child>
<child>
</child>
<style>
<class name="warning"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">General</property>
<!--
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">enable</property>
<child type="suffix">
<object class="GtkSwitch">
<property name="valign">center</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
</object>
</child>
-->
<child>
<object class="AdwActionRow">
<property name="title">port</property>
<child>
<object class="GtkEntry" id="port_entry">
<property name="max-width-chars">5</property>
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
<signal name="changed" handler="handle_port_changed" swapped="true"/>
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
<!-- <property name="title" translatable="yes">port</property> -->
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<!-- <property name="show-apply-button">True</property> -->
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_apply">
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
<property name="icon-name">object-select-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-apply</property>
<style><class name="success"/></style>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_cancel">
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
<property name="icon-name">process-stop-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-cancel</property>
<style><class name="error"/></style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="copy-hostname-button">
<property name="icon-name">edit-copy-symbolic</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Connections</property>
<property name="header-suffix">
<object class="GtkButton">
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</property>
<child>
<object class="GtkListBox" id="client_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow" id="client_placeholder">
<property name="title">No connections!</property>
<property name="subtitle">add a new client via the + button</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@@ -1,141 +0,0 @@
mod client_object;
mod client_row;
mod window;
use std::{env, process, str};
use window::Window;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
use adw::Application;
use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
#[cfg(windows)]
let ret = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
.name("gtk".into())
.spawn(gtk_main)
.unwrap()
.join()
.unwrap();
#[cfg(not(windows))]
let ret = gtk_main();
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret
}
fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.LanMouse")
.build();
app.connect_startup(|_| load_icons());
app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![];
app.run_with_args(&args)
}
fn load_icons() {
let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display);
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket");
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
Ok(conn) => conn,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
log::debug!("connected to lan-mouse-socket");
let (sender, receiver) = async_channel::bounded(10);
gio::spawn_blocking(move || {
while let Some(e) = frontend_rx.next_event() {
match e {
Ok(e) => sender.send_blocking(e).unwrap(),
Err(e) => {
log::error!("{e}");
break;
}
}
}
});
let window = Window::new(app, frontend_tx);
glib::spawn_future_local(clone!(
#[weak]
window,
async move {
loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify {
FrontendEvent::Changed(handle) => {
window.request(FrontendRequest::GetState(handle));
}
FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state);
}
FrontendEvent::Deleted(client) => {
window.delete_client(client);
}
FrontendEvent::State(handle, config, state) => {
window.update_client_config(handle, config);
window.update_client_state(handle, state);
}
FrontendEvent::NoSuchClient(_) => {}
FrontendEvent::Error(e) => {
window.show_toast(e.as_str());
}
FrontendEvent::Enumerate(clients) => {
for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else {
window.new_client(handle, client, state);
}
}
}
FrontendEvent::PortChanged(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
FrontendEvent::CaptureStatus(s) => {
window.set_capture(s.into());
}
FrontendEvent::EmulationStatus(s) => {
window.set_emulation(s.into());
}
}
}
}
));
window.present();
}

View File

@@ -1,16 +0,0 @@
[package]
name = "lan-mouse-ipc"
description = "library for communication between lan-mouse service and frontends"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.107"
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
tokio-stream = { version = "0.1.15", features = ["io-util"] }

View File

@@ -1,89 +0,0 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
io::{self, prelude::*, BufReader, LineWriter, Lines},
thread,
time::Duration,
};
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[cfg(windows)]
use std::net::TcpStream;
pub struct FrontendEventReader {
#[cfg(unix)]
lines: Lines<BufReader<UnixStream>>,
#[cfg(windows)]
lines: Lines<BufReader<TcpStream>>,
}
pub struct FrontendRequestWriter {
#[cfg(unix)]
line_writer: LineWriter<UnixStream>,
#[cfg(windows)]
line_writer: LineWriter<TcpStream>,
}
impl FrontendEventReader {
pub fn next_event(&mut self) -> Option<Result<FrontendEvent, IpcError>> {
match self.lines.next()? {
Err(e) => Some(Err(e.into())),
Ok(l) => Some(serde_json::from_str(l.as_str()).map_err(|e| e.into())),
}
}
}
impl FrontendRequestWriter {
pub fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
let mut json = serde_json::to_string(&request).unwrap();
log::debug!("requesting: {json}");
json.push('\n');
self.line_writer.write_all(json.as_bytes())?;
Ok(())
}
}
pub fn connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
let rx = wait_for_service()?;
let tx = rx.try_clone()?;
let buf_reader = BufReader::new(rx);
let lines = buf_reader.lines();
let line_writer = LineWriter::new(tx);
let reader = FrontendEventReader { lines };
let writer = FrontendRequestWriter { line_writer };
Ok((reader, writer))
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
fn wait_for_service() -> Result<UnixStream, ConnectionError> {
let socket_path = crate::default_socket_path()?;
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = UnixStream::connect(&socket_path) {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
thread::sleep(exponential_back_off(&mut duration));
}
}
#[cfg(windows)]
fn wait_for_service() -> Result<TcpStream, ConnectionError> {
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
break Ok(stream);
}
thread::sleep(exponential_back_off(&mut duration));
}
}
fn exponential_back_off(duration: &mut Duration) -> Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
*duration
}

View File

@@ -1,104 +0,0 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
io,
task::{ready, Poll},
time::Duration,
};
use futures::{Stream, StreamExt};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
use tokio_stream::wrappers::LinesStream;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
pub struct AsyncFrontendEventReader {
#[cfg(unix)]
lines_stream: LinesStream<BufReader<ReadHalf<UnixStream>>>,
#[cfg(windows)]
lines_stream: LinesStream<BufReader<ReadHalf<TcpStream>>>,
}
pub struct AsyncFrontendRequestWriter {
#[cfg(unix)]
tx: WriteHalf<UnixStream>,
#[cfg(windows)]
tx: WriteHalf<TcpStream>,
}
impl Stream for AsyncFrontendEventReader {
type Item = Result<FrontendEvent, IpcError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let line = ready!(self.lines_stream.poll_next_unpin(cx));
let event = line.map(|l| {
l.map_err(Into::<IpcError>::into)
.and_then(|l| serde_json::from_str(l.as_str()).map_err(|e| e.into()))
});
Poll::Ready(event)
}
}
impl AsyncFrontendRequestWriter {
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
let mut json = serde_json::to_string(&request).unwrap();
log::debug!("requesting: {json}");
json.push('\n');
self.tx.write_all(json.as_bytes()).await?;
Ok(())
}
}
pub async fn connect_async(
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
let stream = wait_for_service().await?;
#[cfg(unix)]
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
#[cfg(windows)]
let (rx, tx): (ReadHalf<TcpStream>, WriteHalf<TcpStream>) = tokio::io::split(stream);
let buf_reader = BufReader::new(rx);
let lines = buf_reader.lines();
let lines_stream = LinesStream::new(lines);
let reader = AsyncFrontendEventReader { lines_stream };
let writer = AsyncFrontendRequestWriter { tx };
Ok((reader, writer))
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
async fn wait_for_service() -> Result<UnixStream, ConnectionError> {
let socket_path = crate::default_socket_path()?;
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = UnixStream::connect(&socket_path).await {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
tokio::time::sleep(exponential_back_off(&mut duration)).await;
}
}
#[cfg(windows)]
async fn wait_for_service() -> Result<TcpStream, ConnectionError> {
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252").await {
break Ok(stream);
}
tokio::time::sleep(exponential_back_off(&mut duration)).await;
}
}
fn exponential_back_off(duration: &mut Duration) -> Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
*duration
}

View File

@@ -1,264 +0,0 @@
use std::{
collections::HashSet,
env::VarError,
fmt::Display,
io,
net::{IpAddr, SocketAddr},
str::FromStr,
};
use thiserror::Error;
#[cfg(unix)]
use std::{
env,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
mod connect;
mod connect_async;
mod listen;
pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
pub use listen::AsyncFrontendListener;
#[derive(Debug, Error)]
pub enum ConnectionError {
#[error(transparent)]
SocketPath(#[from] SocketPathError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Debug, Error)]
pub enum ListenerCreationError {
#[error("could not determine socket-path: `{0}`")]
SocketPath(#[from] SocketPathError),
#[error("service already running!")]
AlreadyRunning,
#[error("failed to bind lan-mouse socket: `{0}`")]
Bind(io::Error),
}
#[derive(Debug, Error)]
pub enum IpcError {
#[error("io error occured: `{0}`")]
Io(#[from] io::Error),
#[error("invalid json: `{0}`")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Connection(#[from] ConnectionError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
}
pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
#[default]
Left,
Right,
Top,
Bottom,
}
#[derive(Debug, Error)]
#[error("not a valid position: {pos}")]
pub struct PositionParseError {
pos: String,
}
impl FromStr for Position {
type Err = PositionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
"top" => Ok(Self::Top),
"bottom" => Ok(Self::Bottom),
_ => Err(PositionParseError { pos: s.into() }),
}
}
}
impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
}
)
}
}
impl TryFrom<&str> for Position {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"left" => Ok(Position::Left),
"right" => Ok(Position::Right),
"top" => Ok(Position::Top),
"bottom" => Ok(Position::Bottom),
_ => Err(()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
/// hostname of this client
pub hostname: Option<String>,
/// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16,
/// position of a client on screen
pub pos: Position,
/// enter hook
pub cmd: Option<String>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
port: DEFAULT_PORT,
hostname: Default::default(),
fix_ips: Default::default(),
pos: Default::default(),
cmd: None,
}
}
}
pub type ClientHandle = u64;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ClientState {
/// events should be sent to and received from the client
pub active: bool,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings
pub alive: bool,
/// ips from dns
pub dns_ips: Vec<IpAddr>,
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub ips: HashSet<IpAddr>,
/// client has pressed keys
pub has_pressed_keys: bool,
/// dns resolving in progress
pub resolving: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
/// client state has changed, new state must be requested via [`FrontendRequest::GetState`]
Changed(ClientHandle),
/// a client was created
Created(ClientHandle, ClientConfig, ClientState),
/// no such client
NoSuchClient(ClientHandle),
/// state changed
State(ClientHandle, ClientConfig, ClientState),
/// the client was deleted
Deleted(ClientHandle),
/// new port, reason of failure (if failed)
PortChanged(u16, Option<String>),
/// list of all clients, used for initial state synchronization
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
/// an error occured
Error(String),
/// capture status
CaptureStatus(Status),
/// emulation status
EmulationStatus(Status),
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendRequest {
/// activate/deactivate client
Activate(ClientHandle, bool),
/// add a new client
Create,
/// change the listen port (recreate udp listener)
ChangePort(u16),
/// remove a client
Delete(ClientHandle),
/// request an enumeration of all clients
Enumerate(),
/// resolve dns
ResolveDns(ClientHandle),
/// update hostname
UpdateHostname(ClientHandle, Option<String>),
/// update port
UpdatePort(ClientHandle, u16),
/// update position
UpdatePosition(ClientHandle, Position),
/// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>),
/// request the state of the given client
GetState(ClientHandle),
/// request reenabling input capture
EnableCapture,
/// request reenabling input emulation
EnableEmulation,
/// synchronize all state
Sync,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub enum Status {
#[default]
Disabled,
Enabled,
}
impl From<Status> for bool {
fn from(status: Status) -> Self {
match status {
Status::Enabled => true,
Status::Disabled => false,
}
}
}
#[cfg(unix)]
const LAN_MOUSE_SOCKET_NAME: &str = "lan-mouse-socket.sock";
#[derive(Debug, Error)]
pub enum SocketPathError {
#[error("could not determine $XDG_RUNTIME_DIR: `{0}`")]
XdgRuntimeDirNotFound(VarError),
#[error("could not determine $HOME: `{0}`")]
HomeDirNotFound(VarError),
}
#[cfg(all(unix, not(target_os = "macos")))]
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
let xdg_runtime_dir =
env::var("XDG_RUNTIME_DIR").map_err(SocketPathError::XdgRuntimeDirNotFound)?;
Ok(Path::new(xdg_runtime_dir.as_str()).join(LAN_MOUSE_SOCKET_NAME))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
let home = env::var("HOME").map_err(SocketPathError::HomeDirNotFound)?;
Ok(Path::new(home.as_str())
.join("Library")
.join("Caches")
.join(LAN_MOUSE_SOCKET_NAME))
}

View File

@@ -1,147 +0,0 @@
use futures::{stream::SelectAll, Stream, StreamExt};
#[cfg(unix)]
use std::path::PathBuf;
use std::{
io::ErrorKind,
pin::Pin,
task::{Context, Poll},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
use tokio_stream::wrappers::LinesStream;
#[cfg(unix)]
use tokio::net::UnixListener;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use crate::{FrontendEvent, FrontendRequest, IpcError, ListenerCreationError};
pub struct AsyncFrontendListener {
#[cfg(windows)]
listener: TcpListener,
#[cfg(unix)]
listener: UnixListener,
#[cfg(unix)]
socket_path: PathBuf,
#[cfg(unix)]
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<UnixStream>>>>,
#[cfg(windows)]
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<TcpStream>>>>,
#[cfg(unix)]
tx_streams: Vec<WriteHalf<UnixStream>>,
#[cfg(windows)]
tx_streams: Vec<WriteHalf<TcpStream>>,
}
impl AsyncFrontendListener {
pub async fn new() -> Result<Self, ListenerCreationError> {
#[cfg(unix)]
let (socket_path, listener) = {
let socket_path = crate::default_socket_path()?;
log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() {
// try to connect to see if some other instance
// of lan-mouse is already running
match UnixStream::connect(&socket_path).await {
// connected -> lan-mouse is already running
Ok(_) => return Err(ListenerCreationError::AlreadyRunning),
// lan-mouse is not running but a socket was left behind
Err(e) => {
log::debug!("{socket_path:?}: {e} - removing left behind socket");
let _ = std::fs::remove_file(&socket_path);
}
}
}
let listener = match UnixListener::bind(&socket_path) {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(ListenerCreationError::AlreadyRunning)
}
Err(e) => return Err(ListenerCreationError::Bind(e)),
};
(socket_path, listener)
};
#[cfg(windows)]
let listener = match TcpListener::bind("127.0.0.1:5252").await {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(ListenerCreationError::AlreadyRunning)
}
Err(e) => return Err(ListenerCreationError::Bind(e)),
};
let adapter = Self {
listener,
#[cfg(unix)]
socket_path,
line_streams: SelectAll::new(),
tx_streams: vec![],
};
Ok(adapter)
}
pub async fn broadcast(&mut self, notify: FrontendEvent) {
// encode event
let mut json = serde_json::to_string(&notify).unwrap();
json.push('\n');
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if tx.write(json.as_bytes()).await.is_err() {
keep.push(false);
continue;
}
keep.push(true);
}
// could not find a better solution because async
let mut keep = keep.into_iter();
self.tx_streams.retain(|_| keep.next().unwrap());
}
}
#[cfg(unix)]
impl Drop for AsyncFrontendListener {
fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path);
let _ = std::fs::remove_file(&self.socket_path);
}
}
impl Stream for AsyncFrontendListener {
type Item = Result<FrontendRequest, IpcError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Poll::Ready(Some(Ok(l))) = self.line_streams.poll_next_unpin(cx) {
let request = serde_json::from_str(l.as_str()).map_err(|e| e.into());
return Poll::Ready(Some(request));
}
let mut sync = false;
while let Poll::Ready(Ok((stream, _))) = self.listener.poll_accept(cx) {
let (rx, tx) = tokio::io::split(stream);
let buf_reader = BufReader::new(rx);
let lines = buf_reader.lines();
let lines = LinesStream::new(lines);
self.line_streams.push(lines);
self.tx_streams.push(tx);
sync = true;
}
if sync {
Poll::Ready(Some(Ok(FrontendRequest::Sync)))
} else {
Poll::Pending
}
}
}

View File

@@ -1,13 +0,0 @@
[package]
name = "lan-mouse-proto"
description = "network protocol for lan-mouse"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
num_enum = "0.7.2"
thiserror = "2.0.0"
input-event = { path = "../input-event", version = "0.3.0" }
paste = "1.0"

View File

@@ -1,251 +0,0 @@
use input_event::{Event as InputEvent, KeyboardEvent, PointerEvent};
use num_enum::{IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
use paste::paste;
use std::{
fmt::{Debug, Display},
mem::size_of,
};
use thiserror::Error;
/// defines the maximum size an encoded event can take up
/// this is currently the pointer motion event
/// type: u8, time: u32, dx: f64, dy: f64
pub const MAX_EVENT_SIZE: usize = size_of::<u8>() + size_of::<u32>() + 2 * size_of::<f64>();
/// error type for protocol violations
#[derive(Debug, Error)]
pub enum ProtocolError {
/// event type does not exist
#[error("invalid event id: `{0}`")]
InvalidEventId(#[from] TryFromPrimitiveError<EventType>),
}
/// main lan-mouse protocol event type
#[derive(Clone, Copy, Debug)]
pub enum ProtoEvent {
/// notify a client that the cursor entered its region
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
Enter(u32),
/// notify a client that the cursor left its region
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
Leave(u32),
/// acknowledge of an [`ProtoEvent::Enter`] or [`ProtoEvent::Leave`] event
Ack(u32),
/// Input event
Input(InputEvent),
/// Ping event for tracking unresponsive clients.
/// A client has to respond with [`ProtoEvent::Pong`].
Ping,
/// Response to [`ProtoEvent::Ping`]
Pong,
}
impl Display for ProtoEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProtoEvent::Enter(s) => write!(f, "Enter({s})"),
ProtoEvent::Leave(s) => write!(f, "Leave({s})"),
ProtoEvent::Ack(s) => write!(f, "Ack({s})"),
ProtoEvent::Input(e) => write!(f, "{e}"),
ProtoEvent::Ping => write!(f, "ping"),
ProtoEvent::Pong => write!(f, "pong"),
}
}
}
#[derive(TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum EventType {
PointerMotion,
PointerButton,
PointerAxis,
PointerAxisValue120,
KeyboardKey,
KeyboardModifiers,
Ping,
Pong,
Enter,
Leave,
Ack,
}
impl ProtoEvent {
fn event_type(&self) -> EventType {
match self {
ProtoEvent::Input(e) => match e {
InputEvent::Pointer(p) => match p {
PointerEvent::Motion { .. } => EventType::PointerMotion,
PointerEvent::Button { .. } => EventType::PointerButton,
PointerEvent::Axis { .. } => EventType::PointerAxis,
PointerEvent::AxisDiscrete120 { .. } => EventType::PointerAxisValue120,
},
InputEvent::Keyboard(k) => match k {
KeyboardEvent::Key { .. } => EventType::KeyboardKey,
KeyboardEvent::Modifiers { .. } => EventType::KeyboardModifiers,
},
},
ProtoEvent::Ping => EventType::Ping,
ProtoEvent::Pong => EventType::Pong,
ProtoEvent::Enter(_) => EventType::Enter,
ProtoEvent::Leave(_) => EventType::Leave,
ProtoEvent::Ack(_) => EventType::Ack,
}
}
}
impl TryFrom<[u8; MAX_EVENT_SIZE]> for ProtoEvent {
type Error = ProtocolError;
fn try_from(buf: [u8; MAX_EVENT_SIZE]) -> Result<Self, Self::Error> {
let mut buf = &buf[..];
let event_type = decode_u8(&mut buf)?;
match EventType::try_from(event_type)? {
EventType::PointerMotion => {
Ok(Self::Input(InputEvent::Pointer(PointerEvent::Motion {
time: decode_u32(&mut buf)?,
dx: decode_f64(&mut buf)?,
dy: decode_f64(&mut buf)?,
})))
}
EventType::PointerButton => {
Ok(Self::Input(InputEvent::Pointer(PointerEvent::Button {
time: decode_u32(&mut buf)?,
button: decode_u32(&mut buf)?,
state: decode_u32(&mut buf)?,
})))
}
EventType::PointerAxis => Ok(Self::Input(InputEvent::Pointer(PointerEvent::Axis {
time: decode_u32(&mut buf)?,
axis: decode_u8(&mut buf)?,
value: decode_f64(&mut buf)?,
}))),
EventType::PointerAxisValue120 => Ok(Self::Input(InputEvent::Pointer(
PointerEvent::AxisDiscrete120 {
axis: decode_u8(&mut buf)?,
value: decode_i32(&mut buf)?,
},
))),
EventType::KeyboardKey => Ok(Self::Input(InputEvent::Keyboard(KeyboardEvent::Key {
time: decode_u32(&mut buf)?,
key: decode_u32(&mut buf)?,
state: decode_u8(&mut buf)?,
}))),
EventType::KeyboardModifiers => Ok(Self::Input(InputEvent::Keyboard(
KeyboardEvent::Modifiers {
depressed: decode_u32(&mut buf)?,
latched: decode_u32(&mut buf)?,
locked: decode_u32(&mut buf)?,
group: decode_u32(&mut buf)?,
},
))),
EventType::Ping => Ok(Self::Ping),
EventType::Pong => Ok(Self::Pong),
EventType::Enter => Ok(Self::Enter(decode_u32(&mut buf)?)),
EventType::Leave => Ok(Self::Leave(decode_u32(&mut buf)?)),
EventType::Ack => Ok(Self::Ack(decode_u32(&mut buf)?)),
}
}
}
impl From<ProtoEvent> for ([u8; MAX_EVENT_SIZE], usize) {
fn from(event: ProtoEvent) -> Self {
let mut buf = [0u8; MAX_EVENT_SIZE];
let mut len = 0usize;
{
let mut buf = &mut buf[..];
let buf = &mut buf;
let len = &mut len;
encode_u8(buf, len, event.event_type() as u8);
match event {
ProtoEvent::Input(event) => match event {
InputEvent::Pointer(p) => match p {
PointerEvent::Motion { time, dx, dy } => {
encode_u32(buf, len, time);
encode_f64(buf, len, dx);
encode_f64(buf, len, dy);
}
PointerEvent::Button {
time,
button,
state,
} => {
encode_u32(buf, len, time);
encode_u32(buf, len, button);
encode_u32(buf, len, state);
}
PointerEvent::Axis { time, axis, value } => {
encode_u32(buf, len, time);
encode_u8(buf, len, axis);
encode_f64(buf, len, value);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
encode_u8(buf, len, axis);
encode_i32(buf, len, value);
}
},
InputEvent::Keyboard(k) => match k {
KeyboardEvent::Key { time, key, state } => {
encode_u32(buf, len, time);
encode_u32(buf, len, key);
encode_u8(buf, len, state);
}
KeyboardEvent::Modifiers {
depressed,
latched,
locked,
group,
} => {
encode_u32(buf, len, depressed);
encode_u32(buf, len, latched);
encode_u32(buf, len, locked);
encode_u32(buf, len, group);
}
},
},
ProtoEvent::Ping => {}
ProtoEvent::Pong => {}
ProtoEvent::Enter(serial) => encode_u32(buf, len, serial),
ProtoEvent::Leave(serial) => encode_u32(buf, len, serial),
ProtoEvent::Ack(serial) => encode_u32(buf, len, serial),
}
}
(buf, len)
}
}
macro_rules! decode_impl {
($t:ty) => {
paste! {
fn [<decode_ $t>](data: &mut &[u8]) -> Result<$t, ProtocolError> {
let (int_bytes, rest) = data.split_at(size_of::<$t>());
*data = rest;
Ok($t::from_be_bytes(int_bytes.try_into().unwrap()))
}
}
};
}
decode_impl!(u8);
decode_impl!(u32);
decode_impl!(i32);
decode_impl!(f64);
macro_rules! encode_impl {
($t:ty) => {
paste! {
fn [<encode_ $t>](buf: &mut &mut [u8], amt: &mut usize, n: $t) {
let src = n.to_be_bytes();
let data = std::mem::take(buf);
let (int_bytes, rest) = data.split_at_mut(size_of::<$t>());
int_bytes.copy_from_slice(&src);
*amt += size_of::<$t>();
*buf = rest
}
}
};
}
encode_impl!(u8);
encode_impl!(u32);
encode_impl!(i32);
encode_impl!(f64);

View File

@@ -2,20 +2,14 @@
rustPlatform,
lib,
pkgs,
}: let
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
pname = cargoToml.package.name;
version = cargoToml.package.version;
in
}:
rustPlatform.buildRustPackage {
pname = pname;
version = version;
pname = "lan-mouse";
version = "0.7.0";
nativeBuildInputs = with pkgs; [
git
pkg-config
cmake
makeWrapper
buildPackages.gtk4
];
@@ -24,14 +18,12 @@ rustPlatform.buildRustPackage {
gtk4
libadwaita
xorg.libXtst
] ++ lib.optionals stdenv.isDarwin
(with darwin.apple_sdk_11_0.frameworks; [
CoreGraphics
ApplicationServices
]);
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk_11_0.frameworks.CoreGraphics
];
src = builtins.path {
name = pname;
name = "lan-mouse";
path = lib.cleanSource ../.;
};
@@ -40,22 +32,13 @@ rustPlatform.buildRustPackage {
# Set Environment Variables
RUST_BACKTRACE = "full";
# Needed to enable support for SVG icons in GTK
postInstall = ''
wrapProgram "$out/bin/lan-mouse" \
--set GDK_PIXBUF_MODULE_FILE ${pkgs.librsvg.out}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
install -Dm444 *.desktop -t $out/share/applications
install -Dm444 lan-mouse-gtk/resources/*.svg -t $out/share/icons/hicolor/scalable/apps
'';
meta = with lib; {
description = "Lan Mouse is a mouse and keyboard sharing software";
longDescription = ''
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).
'';
mainProgram = pname;
mainProgram = "lan-mouse";
platforms = platforms.all;
};
}

View File

@@ -44,7 +44,6 @@
<child>
<object class="GtkEntry" id="port">
<!-- <property name="title" translatable="yes">port</property> -->
<property name="max-width-chars">5</property>
<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

167
resources/window.ui Normal file
View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<menu id="main-menu">
<item>
<attribute name="label" translatable="yes">_Close window</attribute>
<attribute name="action">window.close</attribute>
</item>
</menu>
<template class="LanMouseWindow" parent="AdwApplicationWindow">
<property name="width-request">600</property>
<property name="height-request">700</property>
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child type="top">
<object class="AdwHeaderBar">
<child type ="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">de.feschber.LanMouse</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">600</property>
<property name="tightening-threshold">0</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">General</property>
<!--
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">enable</property>
<child type="suffix">
<object class="GtkSwitch">
<property name="valign">center</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
</object>
</child>
-->
<child>
<object class="AdwActionRow">
<property name="title">port</property>
<child>
<object class="GtkEntry" id="port_entry">
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
<signal name="changed" handler="handle_port_changed" swapped="true"/>
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
<!-- <property name="title" translatable="yes">port</property> -->
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<!-- <property name="show-apply-button">True</property> -->
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_apply">
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
<property name="icon-name">object-select-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-apply</property>
<style><class name="success"/></style>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_cancel">
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
<property name="icon-name">process-stop-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-cancel</property>
<style><class name="error"/></style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="copy-hostname-button">
<property name="icon-name">edit-copy-symbolic</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Connections</property>
<property name="header-suffix">
<object class="GtkButton">
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</property>
<child>
<object class="GtkListBox" id="client_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow" id="client_placeholder">
<property name="title">No connections!</property>
<property name="subtitle">add a new client via the + button</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

78
src/capture.rs Normal file
View File

@@ -0,0 +1,78 @@
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<()>;
}

42
src/capture/dummy.rs Normal file
View File

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

577
src/capture/libei.rs Normal file
View File

@@ -0,0 +1,577 @@
use anyhow::{anyhow, Result};
use ashpd::{
desktop::{
input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones},
ResponseError, Session,
},
enumflags2::BitFlags,
};
use futures::StreamExt;
use reis::{
ei::{self, keyboard::KeyState},
eis::button::ButtonState,
event::{DeviceCapability, EiEvent},
tokio::{EiConvertEventStream, EiEventStream},
};
use std::{
cell::Cell,
collections::HashMap,
io,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
task::{ready, Context, Poll},
};
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use futures_core::Stream;
use once_cell::sync::Lazy;
use crate::{
capture::InputCapture as LanMouseInputCapture,
client::{ClientEvent, ClientHandle, Position},
event::{Event, KeyboardEvent, PointerEvent},
};
#[derive(Debug)]
enum ProducerEvent {
Release,
ClientEvent(ClientEvent),
}
#[allow(dead_code)]
pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>,
libei_task: JoinHandle<Result<()>>,
event_rx: tokio::sync::mpsc::Receiver<(ClientHandle, Event)>,
notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>,
}
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
});
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
let (x, y) = (r.x_offset(), r.y_offset());
let (width, height) = (r.width() as i32, r.height() as i32);
match pos {
Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive
Position::Right => (x + width, y, x + width, y + height - 1),
Position::Top => (x, y, x + width - 1, y),
Position::Bottom => (x, y + height, x + width - 1, y + height),
}
}
fn select_barriers(
zones: &Zones,
clients: &Vec<(ClientHandle, Position)>,
next_barrier_id: &mut u32,
) -> (Vec<Barrier>, HashMap<BarrierID, ClientHandle>) {
let mut client_for_barrier = HashMap::new();
let mut barriers: Vec<Barrier> = vec![];
for (handle, pos) in clients {
let mut client_barriers = zones
.regions()
.iter()
.map(|r| {
let id = *next_barrier_id;
*next_barrier_id = id + 1;
let position = pos_to_barrier(r, *pos);
client_for_barrier.insert(id, *handle);
Barrier::new(id, position)
})
.collect();
barriers.append(&mut client_barriers);
}
(barriers, client_for_barrier)
}
async fn update_barriers(
input_capture: &InputCapture<'_>,
session: &Session<'_>,
active_clients: &Vec<(ClientHandle, Position)>,
next_barrier_id: &mut u32,
) -> Result<HashMap<BarrierID, ClientHandle>> {
let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}");
let (barriers, id_map) = select_barriers(&zones, active_clients, next_barrier_id);
log::debug!("barriers: {barriers:?}");
log::debug!("client for barrier id: {id_map:?}");
let response = input_capture
.set_pointer_barriers(session, &barriers, zones.zone_set())
.await?;
let response = response.response()?;
log::debug!("{response:?}");
Ok(id_map)
}
impl<'a> Drop for LibeiInputCapture<'a> {
fn drop(&mut self) {
self.libei_task.abort();
}
}
async fn create_session<'a>(
input_capture: &'a InputCapture<'a>,
) -> Result<(Session<'a>, BitFlags<Capabilities>)> {
log::debug!("creating input capture session");
let (session, capabilities) = loop {
match input_capture
.create_session(
&ashpd::WindowIdentifier::default(),
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
)
.await
{
Ok(s) => break s,
Err(ashpd::Error::Response(ResponseError::Cancelled)) => continue,
o => o?,
};
};
log::debug!("capabilities: {capabilities:?}");
Ok((session, capabilities))
}
async fn connect_to_eis(
input_capture: &InputCapture<'_>,
session: &Session<'_>,
) -> Result<(ei::Context, EiConvertEventStream)> {
log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?;
// create unix stream from fd
let stream = UnixStream::from(fd);
stream.set_nonblocking(true)?;
// create ei context
let context = ei::Context::new(stream)?;
let mut event_stream = EiEventStream::new(context.clone())?;
let response = match reis::tokio::ei_handshake(
&mut event_stream,
"de.feschber.LanMouse",
ei::handshake::ContextType::Receiver,
&INTERFACES,
)
.await
{
Ok(res) => res,
Err(e) => return Err(anyhow!("ei handshake failed: {e:?}")),
};
let event_stream = EiConvertEventStream::new(event_stream, response.serial);
Ok((context, event_stream))
}
async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream,
context: ei::Context,
event_tx: Sender<(ClientHandle, Event)>,
current_client: Rc<Cell<Option<ClientHandle>>>,
) -> Result<()> {
loop {
let ei_event = match ei_event_stream.next().await {
Some(Ok(event)) => event,
Some(Err(e)) => return Err(anyhow!("libei connection closed: {e:?}")),
None => return Err(anyhow!("libei connection closed")),
};
log::trace!("from ei: {ei_event:?}");
let client = current_client.get();
handle_ei_event(ei_event, client, &context, &event_tx).await;
}
}
async fn wait_for_active_client(
notify_rx: &mut Receiver<ProducerEvent>,
active_clients: &mut Vec<(ClientHandle, Position)>,
) -> Result<()> {
// wait for a client update
while let Some(producer_event) = notify_rx.recv().await {
if let ProducerEvent::ClientEvent(c) = producer_event {
handle_producer_event(ProducerEvent::ClientEvent(c), active_clients)?;
break;
}
}
Ok(())
}
impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> Result<Self> {
let input_capture = Box::pin(InputCapture::new().await?);
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 (event_tx, event_rx) = tokio::sync::mpsc::channel(32);
let (notify_tx, mut notify_rx) = tokio::sync::mpsc::channel(32);
let libei_task = tokio::task::spawn_local(async move {
/* safety: libei_task does not outlive Self */
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 {
input_capture,
event_rx,
libei_task,
notify_tx,
};
Ok(producer)
}
}
async fn release_capture(
input_capture: &InputCapture<'_>,
session: &Session<'_>,
activated: Activated,
current_client: ClientHandle,
active_clients: &[(ClientHandle, Position)],
) -> Result<()> {
log::debug!("releasing input capture {}", activated.activation_id());
let (x, y) = activated.cursor_position();
let pos = active_clients
.iter()
.filter(|(c, _)| *c == current_client)
.map(|(_, p)| p)
.next()
.unwrap(); // FIXME
let (dx, dy) = match pos {
// offset cursor position to not enter again immediately
Position::Left => (1., 0.),
Position::Right => (-1., 0.),
Position::Top => (0., 1.),
Position::Bottom => (0., -1.),
};
// release 1px to the right of the entered zone
let cursor_position = (x as f64 + dx, y as f64 + dy);
input_capture
.release(session, activated.activation_id(), cursor_position)
.await?;
Ok(())
}
fn handle_producer_event(
producer_event: ProducerEvent,
active_clients: &mut Vec<(ClientHandle, Position)>,
) -> Result<bool> {
log::debug!("handling event: {producer_event:?}");
let updated = match producer_event {
ProducerEvent::Release => false,
ProducerEvent::ClientEvent(ClientEvent::Create(c, p)) => {
active_clients.push((c, p));
true
}
ProducerEvent::ClientEvent(ClientEvent::Destroy(c)) => {
active_clients.retain(|(h, _)| *h != c);
true
}
};
Ok(updated)
}
async fn handle_ei_event(
ei_event: EiEvent,
current_client: Option<ClientHandle>,
context: &ei::Context,
event_tx: &Sender<(ClientHandle, Event)>,
) {
match ei_event {
EiEvent::SeatAdded(s) => {
s.seat.bind_capabilities(&[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
]);
context.flush().unwrap();
}
EiEvent::SeatRemoved(_) => {}
EiEvent::DeviceAdded(_) => {}
EiEvent::DeviceRemoved(_) => {}
EiEvent::DevicePaused(_) => {}
EiEvent::DeviceResumed(_) => {}
EiEvent::KeyboardModifiers(mods) => {
let modifier_event = KeyboardEvent::Modifiers {
mods_depressed: mods.depressed,
mods_latched: mods.latched,
mods_locked: mods.locked,
group: mods.group,
};
if let Some(current_client) = current_client {
event_tx
.send((current_client, Event::Keyboard(modifier_event)))
.await
.unwrap();
}
}
EiEvent::Frame(_) => {}
EiEvent::DeviceStartEmulating(_) => {
log::debug!("START EMULATING =============>");
}
EiEvent::DeviceStopEmulating(_) => {
log::debug!("==================> STOP EMULATING");
}
EiEvent::PointerMotion(motion) => {
let motion_event = PointerEvent::Motion {
time: motion.time as u32,
relative_x: motion.dx as f64,
relative_y: motion.dy as f64,
};
if let Some(current_client) = current_client {
event_tx
.send((current_client, Event::Pointer(motion_event)))
.await
.unwrap();
}
}
EiEvent::PointerMotionAbsolute(_) => {}
EiEvent::Button(button) => {
let button_event = PointerEvent::Button {
time: button.time as u32,
button: button.button,
state: match button.state {
ButtonState::Released => 0,
ButtonState::Press => 1,
},
};
if let Some(current_client) = current_client {
event_tx
.send((current_client, Event::Pointer(button_event)))
.await
.unwrap();
}
}
EiEvent::ScrollDelta(delta) => {
if let Some(handle) = current_client {
let mut events = vec![];
if delta.dy != 0. {
events.push(PointerEvent::Axis {
time: 0,
axis: 0,
value: delta.dy as f64,
});
}
if delta.dx != 0. {
events.push(PointerEvent::Axis {
time: 0,
axis: 1,
value: delta.dx as f64,
});
}
for event in events {
event_tx
.send((handle, Event::Pointer(event)))
.await
.unwrap();
}
}
}
EiEvent::ScrollStop(_) => {}
EiEvent::ScrollCancel(_) => {}
EiEvent::ScrollDiscrete(scroll) => {
if scroll.discrete_dy != 0 {
let event = PointerEvent::AxisDiscrete120 {
axis: 0,
value: scroll.discrete_dy,
};
if let Some(current_client) = current_client {
event_tx
.send((current_client, Event::Pointer(event)))
.await
.unwrap();
}
}
if scroll.discrete_dx != 0 {
let event = PointerEvent::AxisDiscrete120 {
axis: 1,
value: scroll.discrete_dx,
};
if let Some(current_client) = current_client {
event_tx
.send((current_client, Event::Pointer(event)))
.await
.unwrap();
}
};
}
EiEvent::KeyboardKey(key) => {
let key_event = KeyboardEvent::Key {
key: key.key,
state: match key.state {
KeyState::Press => 1,
KeyState::Released => 0,
},
time: key.time as u32,
};
if let Some(current_client) = current_client {
event_tx
.send((current_client, Event::Keyboard(key_event)))
.await
.unwrap();
}
}
EiEvent::TouchDown(_) => {}
EiEvent::TouchUp(_) => {}
EiEvent::TouchMotion(_) => {}
EiEvent::Disconnected(d) => {
log::error!("disconnect: {d:?}");
}
}
}
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
fn notify(&mut self, event: ClientEvent) -> io::Result<()> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("notifying {event:?}");
let _ = notify_tx.send(ProducerEvent::ClientEvent(event)).await;
log::debug!("done !");
});
Ok(())
}
fn release(&mut self) -> io::Result<()> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("notifying Release");
let _ = notify_tx.send(ProducerEvent::Release).await;
});
Ok(())
}
}
impl<'a> Stream for LibeiInputCapture<'a> {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None),
Some(e) => Poll::Ready(Some(Ok(e))),
}
}
}

33
src/capture/macos.rs Normal file
View File

@@ -0,0 +1,33 @@
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,10 +1,16 @@
use async_trait::async_trait;
use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle, Position},
};
use anyhow::{anyhow, Result};
use futures_core::Stream;
use memmap::MmapOptions;
use std::{
collections::VecDeque,
env,
io::{self, ErrorKind},
os::fd::{AsFd, RawFd},
os::fd::{AsFd, OwnedFd, RawFd},
pin::Pin,
task::{ready, Context, Poll},
};
@@ -13,8 +19,8 @@ use tokio::io::unix::AsyncFd;
use std::{
fs::File,
io::{BufWriter, Write},
os::unix::prelude::AsRawFd,
sync::Arc,
os::unix::prelude::{AsRawFd, FromRawFd},
rc::Rc,
};
use wayland_protocols::{
@@ -58,20 +64,15 @@ use wayland_client::{
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use tempfile;
use crate::{CaptureError, CaptureEvent};
use super::{
error::{LayerShellCaptureCreationError, WaylandBindError},
Capture, Position,
};
use crate::event::{Event, KeyboardEvent, PointerEvent};
struct Globals {
compositor: wl_compositor::WlCompositor,
pointer_constraints: ZwpPointerConstraintsV1,
relative_pointer_manager: ZwpRelativePointerManagerV1,
shortcut_inhibit_manager: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1,
seat: wl_seat::WlSeat,
shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1,
@@ -102,15 +103,14 @@ struct State {
pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
active_windows: Vec<Arc<Window>>,
focused: Option<Arc<Window>>,
client_for_window: Vec<(Rc<Window>, ClientHandle)>,
focused: Option<(Rc<Window>, ClientHandle)>,
g: Globals,
wayland_fd: RawFd,
wayland_fd: OwnedFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: VecDeque<(Position, CaptureEvent)>,
pending_events: VecDeque<(ClientHandle, Event)>,
output_info: Vec<(WlOutput, OutputInfo)>,
scroll_discrete_pending: bool,
}
struct Inner {
@@ -120,11 +120,11 @@ struct Inner {
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.state.wayland_fd
self.state.wayland_fd.as_raw_fd()
}
}
pub struct LayerShellInputCapture(AsyncFd<Inner>);
pub struct WaylandInputCapture(AsyncFd<Inner>);
struct Window {
buffer: wl_buffer::WlBuffer,
@@ -256,47 +256,65 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
}
}
impl LayerShellInputCapture {
pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
let conn = Connection::connect_to_env()?;
let (g, mut queue) = registry_queue_init::<State>(&conn)?;
impl WaylandInputCapture {
pub fn new() -> Result<Self> {
let conn = match Connection::connect_to_env() {
Ok(c) => c,
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 compositor: wl_compositor::WlCompositor = g
.bind(&qh, 4..=5, ())
.map_err(|e| WaylandBindError::new(e, "wl_compositor 4..=5"))?;
let xdg_output_manager: ZxdgOutputManagerV1 = g
.bind(&qh, 1..=3, ())
.map_err(|e| WaylandBindError::new(e, "xdg_output_manager 1..=3"))?;
let shm: wl_shm::WlShm = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "wl_shm"))?;
let layer_shell: ZwlrLayerShellV1 = g
.bind(&qh, 3..=4, ())
.map_err(|e| WaylandBindError::new(e, "wlr_layer_shell 3..=4"))?;
let seat: wl_seat::WlSeat = g
.bind(&qh, 7..=8, ())
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) {
Ok(compositor) => compositor,
Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")),
};
let xdg_output_manager: ZxdgOutputManagerV1 = match g.bind(&qh, 1..=3, ()) {
Ok(xdg_output_manager) => xdg_output_manager,
Err(_) => return Err(anyhow!("xdg_output not supported!")),
};
let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) {
Ok(wl_shm) => wl_shm,
Err(_) => return Err(anyhow!("wl_shm v1 not supported")),
};
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: Result<
ZwpKeyboardShortcutsInhibitManagerV1,
WaylandBindError,
> = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"));
// layer-shell backend still works without this protocol so we make it an optional dependency
if let Err(e) = &shortcut_inhibit_manager {
log::warn!("shortcut_inhibit_manager not supported: {e}\nkeybinds handled by the compositor will not be passed
to the client");
}
let shortcut_inhibit_manager = shortcut_inhibit_manager.ok();
let outputs = vec![];
let g = Globals {
@@ -314,7 +332,10 @@ impl LayerShellInputCapture {
// flush outgoing events
queue.flush()?;
let wayland_fd = queue.as_fd().as_raw_fd();
// prepare reading wayland events
let read_guard = queue.prepare_read().unwrap(); // there can not yet be events to dispatch
let wayland_fd = read_guard.connection_fd().try_clone_to_owned().unwrap();
std::mem::drop(read_guard);
let mut state = State {
pointer: None,
@@ -323,14 +344,13 @@ impl LayerShellInputCapture {
pointer_lock: None,
rel_pointer: None,
shortcut_inhibitor: None,
active_windows: Vec::new(),
client_for_window: Vec::new(),
focused: None,
qh,
wayland_fd,
read_guard: None,
pending_events: VecDeque::new(),
output_info: vec![],
scroll_discrete_pending: false,
};
// dispatch registry to () again, in order to read all wl_outputs
@@ -370,18 +390,23 @@ impl LayerShellInputCapture {
let inner = AsyncFd::new(Inner { queue, state })?;
Ok(LayerShellInputCapture(inner))
Ok(WaylandInputCapture(inner))
}
fn add_client(&mut self, pos: Position) {
self.0.get_mut().state.add_client(pos);
fn add_client(&mut self, handle: ClientHandle, pos: Position) {
self.0.get_mut().state.add_client(handle, pos);
}
fn delete_client(&mut self, pos: Position) {
fn delete_client(&mut self, handle: ClientHandle) {
let inner = self.0.get_mut();
// remove all windows corresponding to this client
while let Some(i) = inner.state.active_windows.iter().position(|w| w.pos == pos) {
inner.state.active_windows.remove(i);
while let Some(i) = inner
.state
.client_for_window
.iter()
.position(|(_, c)| *c == handle)
{
inner.state.client_for_window.remove(i);
inner.state.focused = None;
}
}
@@ -395,7 +420,7 @@ impl State {
serial: u32,
qh: &QueueHandle<State>,
) {
let window = self.focused.as_ref().unwrap();
let (window, _) = self.focused.as_ref().unwrap();
// hide the cursor
pointer.set_cursor(serial, None, 0, 0);
@@ -428,17 +453,19 @@ impl State {
}
// capture modifier keys
if let Some(shortcut_inhibit_manager) = &self.g.shortcut_inhibit_manager {
if self.shortcut_inhibitor.is_none() {
self.shortcut_inhibitor =
Some(shortcut_inhibit_manager.inhibit_shortcuts(surface, &self.g.seat, qh, ()));
}
if self.shortcut_inhibitor.is_none() {
self.shortcut_inhibitor = Some(self.g.shortcut_inhibit_manager.inhibit_shortcuts(
surface,
&self.g.seat,
qh,
(),
));
}
}
fn ungrab(&mut self) {
// get focused client
let window = match self.focused.as_ref() {
let (window, _client) = match self.focused.as_ref() {
Some(focused) => focused,
None => return,
};
@@ -468,23 +495,27 @@ impl State {
}
}
fn add_client(&mut self, pos: Position) {
fn add_client(&mut self, client: ClientHandle, pos: Position) {
let outputs = get_output_configuration(self, pos);
log::debug!("outputs: {outputs:?}");
outputs.iter().for_each(|(o, i)| {
let window = Window::new(self, &self.qh, o, pos, i.size);
let window = Arc::new(window);
self.active_windows.push(window);
let window = Rc::new(window);
self.client_for_window.push((window, client));
});
}
fn update_windows(&mut self) {
log::debug!("updating windows");
log::debug!("output info: {:?}", self.output_info);
let clients: Vec<_> = self.active_windows.drain(..).map(|w| w.pos).collect();
for pos in clients {
self.add_client(pos);
let clients: Vec<_> = self
.client_for_window
.drain(..)
.map(|(w, c)| (c, w.pos))
.collect();
for (client, pos) in clients {
self.add_client(client, pos);
}
}
}
@@ -556,34 +587,30 @@ impl Inner {
}
}
#[async_trait]
impl Capture for LayerShellInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
self.add_client(pos);
impl InputCapture for WaylandInputCapture {
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> {
match client_event {
ClientEvent::Create(handle, pos) => {
self.add_client(handle, pos);
}
ClientEvent::Destroy(handle) => {
self.delete_client(handle);
}
}
let inner = self.0.get_mut();
Ok(inner.flush_events()?)
inner.flush_events()
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
self.delete_client(pos);
let inner = self.0.get_mut();
Ok(inner.flush_events()?)
}
async fn release(&mut self) -> Result<(), CaptureError> {
fn release(&mut self) -> io::Result<()> {
log::debug!("releasing pointer");
let inner = self.0.get_mut();
inner.state.ungrab();
Ok(inner.flush_events()?)
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
inner.flush_events()
}
}
impl Stream for LayerShellInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
impl Stream for WaylandInputCapture {
type Item = io::Result<(ClientHandle, Event)>;
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() {
@@ -601,7 +628,7 @@ impl Stream for LayerShellInputCapture {
// prepare next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
Err(e) => return Poll::Ready(Some(Err(e))),
}
}
@@ -611,14 +638,14 @@ impl Stream for LayerShellInputCapture {
// flush outgoing events
if let Err(e) = inner.flush_events() {
if e.kind() != ErrorKind::WouldBlock {
return Poll::Ready(Some(Err(e.into())));
return Poll::Ready(Some(Err(e)));
}
}
// prepare for the next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
Err(e) => return Poll::Ready(Some(Err(e))),
}
}
@@ -648,16 +675,10 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Pointer) {
if let Some(p) = state.pointer.take() {
p.release();
}
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() {
state.pointer.replace(seat.get_pointer(qh, ()));
}
if capabilities.contains(wl_seat::Capability::Keyboard) {
if let Some(k) = state.keyboard.take() {
k.release();
}
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() {
seat.get_keyboard(qh, ());
}
}
@@ -682,20 +703,23 @@ impl Dispatch<WlPointer, ()> for State {
} => {
// get client corresponding to the focused surface
{
if let Some(window) = app.active_windows.iter().find(|w| w.surface == surface) {
app.focused = Some(window.clone());
if let Some((window, client)) = app
.client_for_window
.iter()
.find(|(w, _c)| w.surface == surface)
{
app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial, qh);
} else {
return;
}
}
let pos = app
.active_windows
let (_, client) = app
.client_for_window
.iter()
.find(|w| w.surface == surface)
.map(|w| w.pos)
.find(|(w, _c)| w.surface == surface)
.unwrap();
app.pending_events.push_back((pos, CaptureEvent::Begin));
app.pending_events.push_back((*client, Event::Enter()));
}
wl_pointer::Event::Leave { .. } => {
/* There are rare cases, where when a window is opened in
@@ -716,43 +740,35 @@ impl Dispatch<WlPointer, ()> for State {
button,
state,
} => {
let window = app.focused.as_ref().unwrap();
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
*client,
Event::Pointer(PointerEvent::Button {
time,
button,
state: u32::from(state),
})),
}),
));
}
wl_pointer::Event::Axis { time, axis, value } => {
let window = app.focused.as_ref().unwrap();
if app.scroll_discrete_pending {
// each axisvalue120 event is coupled with
// a corresponding axis event, which needs to
// be ignored to not duplicate the scrolling
app.scroll_discrete_pending = false;
} else {
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
})),
));
}
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push_back((
*client,
Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
}),
));
}
wl_pointer::Event::AxisValue120 { axis, value120 } => {
let window = app.focused.as_ref().unwrap();
app.scroll_discrete_pending = true;
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::AxisDiscrete120 {
*client,
Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: u32::from(axis) as u8,
value: value120,
})),
}),
));
}
wl_pointer::Event::Frame {} => {
@@ -774,7 +790,10 @@ impl Dispatch<WlKeyboard, ()> for State {
_: &Connection,
_: &QueueHandle<Self>,
) {
let window = &app.focused;
let (_window, client) = match &app.focused {
Some(focused) => (Some(&focused.0), Some(&focused.1)),
None => (None, None),
};
match event {
wl_keyboard::Event::Key {
serial: _,
@@ -782,14 +801,14 @@ impl Dispatch<WlKeyboard, ()> for State {
key,
state,
} => {
if let Some(window) = window {
if let Some(client) = client {
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
*client,
Event::Keyboard(KeyboardEvent::Key {
time,
key,
state: u32::from(state) as u8,
})),
}),
));
}
}
@@ -800,18 +819,27 @@ impl Dispatch<WlKeyboard, ()> for State {
mods_locked,
group,
} => {
if let Some(window) = window {
if let Some(client) = client {
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers {
depressed: mods_depressed,
latched: mods_latched,
locked: mods_locked,
*client,
Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
})),
}),
));
}
}
wl_keyboard::Event::Keymap {
format: _,
fd,
size: _,
} => {
let fd = unsafe { &File::from_raw_fd(fd.as_raw_fd()) };
let _mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
// TODO keymap
}
_ => (),
}
}
@@ -829,16 +857,21 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
if let zwp_relative_pointer_v1::Event::RelativeMotion {
utime_hi,
utime_lo,
dx_unaccel: dx,
dy_unaccel: dy,
..
dx: _,
dy: _,
dx_unaccel: surface_x,
dy_unaccel: surface_y,
} = event
{
if let Some(window) = &app.focused {
if let Some((_window, client)) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })),
*client,
Event::Pointer(PointerEvent::Motion {
time,
relative_x: surface_x,
relative_y: surface_y,
}),
));
}
}
@@ -855,10 +888,10 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
_: &QueueHandle<Self>,
) {
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
if let Some(window) = app
.active_windows
if let Some((window, _client)) = app
.client_for_window
.iter()
.find(|w| &w.layer_surface == layer_surface)
.find(|(w, _c)| &w.layer_surface == layer_surface)
{
// client corresponding to the layer_surface
let surface = &window.surface;

View File

@@ -1,17 +1,17 @@
use async_trait::async_trait;
use anyhow::Result;
use core::task::{Context, Poll};
use futures::Stream;
use once_cell::unsync::Lazy;
use std::collections::HashSet;
use std::collections::HashMap;
use std::ptr::{addr_of, addr_of_mut};
use futures::executor::block_on;
use std::default::Default;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{mpsc, Mutex};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc;
use std::task::ready;
use std::{pin::Pin, thread};
use std::{io, pin::Pin, thread};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
@@ -32,25 +32,25 @@ use windows::Win32::UI::WindowsAndMessaging::{
WNDPROC,
};
use input_event::{
scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
use crate::client::Position;
use crate::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::{Capture, CaptureError, CaptureEvent, Position};
enum Request {
Create(Position),
Destroy(Position),
}
pub struct WindowsInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>,
event_rx: Receiver<(ClientHandle, Event)>,
msg_thread: Option<std::thread::JoinHandle<()>>,
}
enum EventType {
Request = 0,
ClientEvent = 0,
Release = 1,
Exit = 2,
}
@@ -59,48 +59,29 @@ unsafe fn signal_message_thread(event_type: EventType) {
if let Some(event_tid) = get_event_tid() {
PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap();
} else {
panic!();
log::warn!("lost event");
}
}
#[async_trait]
impl Capture for WindowsInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
impl InputCapture for WindowsInputCapture {
fn notify(&mut self, event: ClientEvent) -> io::Result<()> {
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Create(pos));
}
signal_message_thread(EventType::Request);
EVENT_BUFFER.push(event);
signal_message_thread(EventType::ClientEvent);
}
Ok(())
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Destroy(pos));
}
signal_message_thread(EventType::Request);
}
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
fn release(&mut self) -> io::Result<()> {
unsafe { signal_message_thread(EventType::Release) };
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new());
static mut ACTIVE_CLIENT: Option<Position> = None;
static mut CLIENTS: Lazy<HashSet<Position>> = Lazy::new(HashSet::new);
static mut EVENT_TX: Option<Sender<(Position, CaptureEvent)>> = None;
static mut EVENT_BUFFER: Vec<ClientEvent> = Vec::new();
static mut ACTIVE_CLIENT: Option<ClientHandle> = None;
static mut CLIENT_FOR_POS: Lazy<HashMap<Position, ClientHandle>> = Lazy::new(HashMap::new);
static mut EVENT_TX: Option<Sender<(ClientHandle, Event)>> = None;
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
unsafe fn set_event_tid(tid: u32) {
EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
@@ -115,7 +96,8 @@ unsafe fn get_event_tid() -> Option<u32> {
static mut ENTRY_POINT: (i32, i32) = (0, 0);
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
let mouse_low_level: MSLLHOOKSTRUCT =
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
@@ -151,12 +133,15 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT;
let (dx, dy) = (x - ex, y - ey);
let (dx, dy) = (dx as f64, dy as f64);
Some(PointerEvent::Motion { time: 0, dx, dy })
Some(PointerEvent::Motion {
time: 0,
relative_x: dx as f64,
relative_y: dy as f64,
})
},
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0,
value: -(mouse_low_level.mouseData as i32 >> 16),
value: -(mouse_low_level.mouseData as i32),
}),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16;
@@ -182,7 +167,8 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
}
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = *(lparam.0 as *const KBDLLHOOKSTRUCT);
let kybrdllhookstruct: KBDLLHOOKSTRUCT =
*std::mem::transmute::<LPARAM, *const KBDLLHOOKSTRUCT>(lparam);
let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
@@ -249,7 +235,7 @@ fn clamp_to_display_bounds(prev_point: (i32, i32), point: (i32, i32)) -> (i32, i
(x.clamp(min_x, max_x), y.clamp(min_y, max_y))
}
unsafe fn send_blocking(event: CaptureEvent) {
unsafe fn send_blocking(event: Event) {
if let Some(active) = ACTIVE_CLIENT {
block_on(async move {
let _ = EVENT_TX.as_ref().unwrap().send((active, event)).await;
@@ -261,7 +247,8 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.is_some();
}
let mouse_low_level: MSLLHOOKSTRUCT = *(lparam.0 as *const MSLLHOOKSTRUCT);
let mouse_low_level: MSLLHOOKSTRUCT =
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
static mut PREV_POS: Option<(i32, i32)> = None;
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.unwrap_or(curr_pos);
@@ -281,17 +268,17 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
};
/* check if a client is registered for the barrier */
if !CLIENTS.contains(&pos) {
let Some(client) = CLIENT_FOR_POS.get(&pos) else {
return ret;
}
};
/* update active client and entry point */
ACTIVE_CLIENT.replace(pos);
ACTIVE_CLIENT.replace(*client);
ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
/* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
send_blocking(CaptureEvent::Begin);
send_blocking(Event::Enter());
ret
}
@@ -305,7 +292,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
}
/* get active client if any */
let Some(pos) = ACTIVE_CLIENT else {
let Some(client) = ACTIVE_CLIENT else {
return LRESULT(1);
};
@@ -313,7 +300,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (pos, CaptureEvent::Input(Event::Pointer(pointer_event)));
let event = (client, Event::Pointer(pointer_event));
/* notify mainthread (drop events if sending too fast) */
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
@@ -334,7 +321,7 @@ unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
let Some(key_event) = to_key_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (client, CaptureEvent::Input(Event::Keyboard(key_event)));
let event = (client, Event::Keyboard(key_event));
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
@@ -372,6 +359,7 @@ fn enumerate_displays() -> Vec<RECT> {
break;
}
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
log::info!("{:?}", device.DeviceName);
devices.push(device.DeviceName);
}
}
@@ -493,8 +481,6 @@ fn get_msg() -> Option<MSG> {
}
}
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
fn message_thread(ready_tx: mpsc::Sender<()>) {
unsafe {
set_event_tid(GetCurrentThreadId());
@@ -515,19 +501,13 @@ fn message_thread(ready_tx: mpsc::Sender<()>) {
..Default::default()
};
if WINDOW_CLASS_REGISTERED
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
/* register window class if not yet done so */
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
/* window is used ro receive WM_DISPLAYCHANGE messages */
CreateWindowExW(
let ret = CreateWindowExW(
Default::default(),
w!("lan-mouse-message-window-class"),
w!("lan-mouse-msg-window"),
@@ -540,8 +520,10 @@ fn message_thread(ready_tx: mpsc::Sender<()>) {
HMENU::default(),
instance,
None,
)
.expect("CreateWindowExW");
);
if ret.0 == 0 {
panic!("CreateWindowExW");
}
/* run message loop */
loop {
@@ -549,56 +531,54 @@ fn message_thread(ready_tx: mpsc::Sender<()>) {
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0.is_null() {
if msg.hwnd.0 == 0 {
/* messages sent via PostThreadMessage */
match msg.wParam.0 {
x if x == EventType::Exit as usize => break,
x if x == EventType::Release as usize => {
ACTIVE_CLIENT.take();
}
x if x == EventType::Request as usize => {
let requests = {
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)
x if x == EventType::ClientEvent as usize => {
while let Some(event) = EVENT_BUFFER.pop() {
update_clients(event)
}
}
_ => {}
}
} else {
/* other messages for window_procs */
let _ = TranslateMessage(&msg);
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
}
fn update_clients(request: Request) {
match request {
Request::Create(pos) => {
unsafe { CLIENTS.insert(pos) };
fn update_clients(client_event: ClientEvent) {
match client_event {
ClientEvent::Create(handle, pos) => {
unsafe { CLIENT_FOR_POS.insert(pos, handle) };
}
Request::Destroy(pos) => unsafe {
if let Some(active_pos) = ACTIVE_CLIENT {
if pos == active_pos {
let _ = ACTIVE_CLIENT.take();
ClientEvent::Destroy(handle) => unsafe {
for pos in [
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
] {
if ACTIVE_CLIENT == Some(handle) {
ACTIVE_CLIENT.take();
}
if CLIENT_FOR_POS.get(&pos).copied() == Some(handle) {
CLIENT_FOR_POS.remove(&pos);
}
}
CLIENTS.remove(&pos);
},
}
}
impl WindowsInputCapture {
pub(crate) fn new() -> Self {
pub(crate) fn new() -> Result<Self> {
unsafe {
let (tx, rx) = channel(10);
EVENT_TX.replace(tx);
@@ -606,16 +586,16 @@ impl WindowsInputCapture {
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
/* wait for thread to set its id */
ready_rx.recv().expect("channel closed");
Self {
Ok(Self {
msg_thread,
event_rx: rx,
}
})
}
}
}
impl Stream for WindowsInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None),

39
src/capture/x11.rs Normal file
View File

@@ -0,0 +1,39 @@
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,44 +1,42 @@
use crate::config::Config;
use crate::capture;
use crate::client::{ClientEvent, Position};
use crate::event::{Event, KeyboardEvent};
use anyhow::{anyhow, Result};
use futures::StreamExt;
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
use input_event::{Event, KeyboardEvent};
use tokio::task::LocalSet;
pub async fn run(config: Config) -> Result<(), InputCaptureError> {
pub fn run() -> Result<()> {
log::info!("running input capture test");
log::info!("creating input capture");
let backend = config.capture_backend.map(|b| b.into());
loop {
let mut input_capture = InputCapture::new(backend).await?;
log::info!("creating clients");
input_capture.create(0, Position::Left).await?;
input_capture.create(4, Position::Left).await?;
input_capture.create(1, Position::Right).await?;
input_capture.create(2, Position::Top).await?;
input_capture.create(3, Position::Bottom).await?;
if let Err(e) = do_capture(&mut input_capture).await {
log::warn!("{e} - recreating capture");
}
let _ = input_capture.terminate().await;
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(input_capture_test()))
}
async fn do_capture(input_capture: &mut InputCapture) -> Result<(), CaptureError> {
async fn input_capture_test() -> Result<()> {
log::info!("creating input capture");
let mut input_capture = capture::create().await;
log::info!("creating clients");
input_capture.notify(ClientEvent::Create(0, Position::Left))?;
input_capture.notify(ClientEvent::Create(1, Position::Right))?;
input_capture.notify(ClientEvent::Create(2, Position::Top))?;
input_capture.notify(ClientEvent::Create(3, Position::Bottom))?;
loop {
let (client, event) = input_capture
.next()
.await
.ok_or(CaptureError::EndOfStream)??;
.ok_or(anyhow!("capture stream closed"))??;
let pos = match client {
0 | 4 => Position::Left,
0 => Position::Left,
1 => Position::Right,
2 => Position::Top,
3 => Position::Bottom,
_ => panic!(),
_ => Position::Bottom,
};
log::info!("position: {client} ({pos}), event: {event}");
if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key: 1, .. })) = event {
input_capture.release().await?;
break Ok(());
log::info!("position: {pos}, event: {event}");
if let Event::Keyboard(KeyboardEvent::Key { key: 1, .. }) = event {
input_capture.release()?;
}
}
}

View File

@@ -1,18 +1,172 @@
use std::net::SocketAddr;
use std::{
collections::HashSet,
error::Error,
fmt::Display,
net::{IpAddr, SocketAddr},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use slab::Slab;
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
use crate::config::DEFAULT_PORT;
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
Left,
Right,
Top,
Bottom,
}
impl Default for Position {
fn default() -> Self {
Self::Left
}
}
#[derive(Debug)]
pub struct PositionParseError {
string: String,
}
impl Display for PositionParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not a valid position: {}", self.string)
}
}
impl Error for PositionParseError {}
impl FromStr for Position {
type Err = PositionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
"top" => Ok(Self::Top),
"bottom" => Ok(Self::Bottom),
_ => Err(PositionParseError { string: s.into() }),
}
}
}
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 {
write!(
f,
"{}",
match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
}
)
}
}
impl TryFrom<&str> for Position {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"left" => Ok(Position::Left),
"right" => Ok(Position::Right),
"top" => Ok(Position::Top),
"bottom" => Ok(Position::Bottom),
_ => Err(()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
/// hostname of this client
pub hostname: Option<String>,
/// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16,
/// position of a client on screen
pub pos: Position,
/// enter hook
pub cmd: Option<String>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
port: DEFAULT_PORT,
hostname: Default::default(),
fix_ips: Default::default(),
pos: Default::default(),
cmd: None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
}
pub type ClientHandle = u64;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ClientState {
/// events should be sent to and received from the client
pub active: bool,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings
pub alive: bool,
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub ips: HashSet<IpAddr>,
/// keys currently pressed by this client
pub pressed_keys: HashSet<u32>,
/// dns resolving in progress
pub resolving: bool,
}
#[derive(Default)]
pub struct ClientManager {
clients: Slab<(ClientConfig, ClientState)>,
}
impl Default for ClientManager {
fn default() -> Self {
Self::new()
}
}
impl ClientManager {
pub fn new() -> Self {
let clients = Slab::new();
Self { clients }
}
/// add a new client to this manager
pub fn add_client(&mut self) -> ClientHandle {
self.clients.insert(Default::default()) as ClientHandle
let client_config = Default::default();
let client_state = Default::default();
self.clients.insert((client_config, client_state)) as ClientHandle
}
/// find a client by its address

View File

@@ -1,26 +1,22 @@
use clap::{Parser, ValueEnum};
use anyhow::Result;
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::env::{self, VarError};
use std::fmt::Display;
use std::fs;
use std::collections::HashSet;
use std::env;
use std::net::IpAddr;
use std::{collections::HashSet, io};
use thiserror::Error;
use std::{error::Error, fs};
use toml;
use lan_mouse_ipc::{Position, DEFAULT_PORT};
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;
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub port: Option<u16>,
pub frontend: Option<Frontend>,
pub frontend: Option<String>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
@@ -30,7 +26,6 @@ pub struct ConfigToml {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient {
pub capture_backend: Option<CaptureBackend>,
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
@@ -40,7 +35,7 @@ pub struct TomlClient {
}
impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, ConfigError> {
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?)
@@ -48,7 +43,7 @@ impl ConfigToml {
}
#[derive(Parser, Debug)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None)]
#[command(author, version, about, long_about = None)]
struct CliArgs {
/// the listen port for lan-mouse
#[arg(short, long)]
@@ -56,7 +51,7 @@ struct CliArgs {
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<Frontend>,
frontend: Option<String>,
/// non-default config file location
#[arg(short, long)]
@@ -73,159 +68,16 @@ struct CliArgs {
/// test input emulation
#[arg(long)]
test_emulation: bool,
/// capture backend override
#[arg(long)]
capture_backend: Option<CaptureBackend>,
/// emulation backend override
#[arg(long)]
emulation_backend: Option<EmulationBackend>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[serde(rename = "input-capture-portal")]
InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[serde(rename = "layer-shell")]
LayerShell,
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
#[serde(rename = "windows")]
Windows,
#[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs,
#[serde(rename = "dummy")]
Dummy,
}
impl Display for CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11_capture", 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_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(all(unix, feature = "x11_capture", 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 = "wlroots_emulation", not(target_os = "macos")))]
#[serde(rename = "wlroots")]
Wlroots,
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[serde(rename = "libei")]
Libei,
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[serde(rename = "xdp")]
Xdp,
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
#[serde(rename = "windows")]
Windows,
#[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs,
#[serde(rename = "dummy")]
Dummy,
}
impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => Self::Libei,
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => Self::Xdp,
#[cfg(all(unix, feature = "x11_emulation", 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 = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11_emulation", 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)]
#[derive(Debug, PartialEq, Eq)]
pub enum Frontend {
#[serde(rename = "gtk")]
Gtk,
#[serde(rename = "cli")]
Cli,
}
impl Default for Frontend {
fn default() -> Self {
if cfg!(feature = "gtk") {
Self::Gtk
} else {
Self::Cli
}
}
}
#[derive(Debug)]
pub struct Config {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(TomlClient, Position)>,
@@ -244,21 +96,11 @@ pub struct ConfigClient {
pub enter_hook: Option<String>,
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error(transparent)]
Toml(#[from] toml::de::Error),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Var(#[from] VarError),
}
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config {
pub fn new() -> Result<Self, ConfigError> {
pub fn new() -> Result<Self> {
let args = CliArgs::parse();
let config_file = "config.toml";
#[cfg(unix)]
@@ -287,14 +129,33 @@ impl Config {
Ok(c) => Some(c),
};
let frontend_arg = args.frontend;
let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
let frontend = match args.frontend {
None => match &config_toml {
Some(c) => c.frontend.clone(),
None => None,
},
frontend => frontend,
};
let port = args
.port
.or(config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT);
let frontend = match frontend {
#[cfg(feature = "gtk")]
None => Frontend::Gtk,
#[cfg(not(feature = "gtk"))]
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:?}");
let release_bind = config_toml
@@ -302,14 +163,6 @@ impl Config {
.and_then(|c| c.release_bind.clone())
.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![];
if let Some(config_toml) = config_toml {
@@ -332,8 +185,6 @@ impl Config {
let test_emulation = args.test_emulation;
Ok(Config {
capture_backend,
emulation_backend,
daemon,
frontend,
clients,

View File

@@ -1,63 +1,23 @@
use local_channel::mpsc::Receiver;
use std::net::IpAddr;
use anyhow::Result;
use std::{error::Error, net::IpAddr};
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use hickory_resolver::TokioAsyncResolver;
use crate::server::Server;
use lan_mouse_ipc::ClientHandle;
pub(crate) struct DnsResolver {
pub struct DnsResolver {
resolver: TokioAsyncResolver,
dns_request: Receiver<ClientHandle>,
}
impl DnsResolver {
pub(crate) fn new(dns_request: Receiver<ClientHandle>) -> Result<Self, ResolveError> {
pub(crate) async fn new() -> Result<Self> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
Ok(Self {
resolver,
dns_request,
})
Ok(Self { resolver })
}
async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, ResolveError> {
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = self.resolver.lookup_ip(host).await?;
for ip in response.iter() {
log::info!("{host}: adding ip {ip}");
}
Ok(response.iter().collect())
}
pub(crate) async fn run(mut self, server: Server) {
tokio::select! {
_ = server.cancelled() => {},
_ = self.do_dns(&server) => {},
}
}
async fn do_dns(&mut self, server: &Server) {
loop {
let handle = self.dns_request.recv().await.expect("channel closed");
/* update resolving status */
let hostname = match server.get_hostname(handle) {
Some(hostname) => hostname,
None => continue,
};
log::info!("resolving ({handle}) `{hostname}` ...");
server.set_resolving(handle, true);
let ips = match self.resolve(&hostname).await {
Ok(ips) => ips,
Err(e) => {
log::warn!("could not resolve host '{hostname}': {e}");
vec![]
}
};
server.update_dns_ips(handle, ips);
server.set_resolving(handle, false);
}
}
}

98
src/emulate.rs Normal file
View File

@@ -0,0 +1,98 @@
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())
}

26
src/emulate/dummy.rs Normal file
View File

@@ -0,0 +1,26 @@
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) {}
}

399
src/emulate/libei.rs Normal file
View File

@@ -0,0 +1,399 @@
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) {}
}

304
src/emulate/macos.rs Normal file
View File

@@ -0,0 +1,304 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::emulate::InputEmulation;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use keycode::{KeyMap, KeyMapping};
use std::ops::{Index, IndexMut};
use std::time::Duration;
use tokio::task::AbortHandle;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct MacOSEmulation {
pub event_source: CGEventSource,
repeat_task: Option<AbortHandle>,
button_state: ButtonState,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSEmulation {}
impl MacOSEmulation {
pub fn new() -> Result<Self> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
Ok(e) => e,
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
repeat_task: None,
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let event_source = self.event_source.clone();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(event_source.clone(), key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn key_event(event_source: CGEventSource, key: u16, state: u8) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return;
}
};
event.post(CGEventTapLocation::HID);
}
#[async_trait]
impl InputEmulation for MacOSEmulation {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
// FIXME secondary displays?
let (min_x, min_y, max_x, max_y) = unsafe {
let display = CGMainDisplayID();
let bounds = CGDisplayBounds(display);
let min_x = bounds.origin.x;
let max_x = bounds.origin.x + bounds.size.width;
let min_y = bounds.origin.y;
let max_y = bounds.origin.y + bounds.size.height;
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
};
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return;
}
};
mouse_location.x = (mouse_location.x + relative_x).clamp(min_x, max_x - 1.);
mouse_location.y = (mouse_location.y + relative_y).clamp(min_y, max_y - 1.);
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_X,
relative_x as i64,
);
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_Y,
relative_y as i64,
);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return;
}
};
// store button state
self.button_state[mouse_button] = state == 1;
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return;
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return;
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Frame { .. } => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
Ok(k) => k.mac as CGKeyCode,
Err(_) => {
log::warn!("unable to map key event");
return;
}
};
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.kill_repeat_task(),
}
key_event(self.event_source.clone(), code, state)
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => (),
}
}
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
}

View File

@@ -1,9 +1,9 @@
use super::error::{EmulationError, WindowsEmulationCreationError};
use input_event::{
scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
BTN_RIGHT,
use crate::{
emulate::InputEmulation,
event::{KeyboardEvent, PointerEvent},
scancode,
};
use anyhow::Result;
use async_trait::async_trait;
use std::ops::BitOrAssign;
use std::time::Duration;
@@ -19,28 +19,36 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
};
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use super::{Emulation, EmulationHandle};
use crate::event::{BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub(crate) struct WindowsEmulation {
pub struct WindowsEmulation {
repeat_task: Option<AbortHandle>,
}
impl WindowsEmulation {
pub(crate) fn new() -> Result<Self, WindowsEmulationCreationError> {
pub fn new() -> Result<Self> {
Ok(Self { repeat_task: None })
}
}
#[async_trait]
impl Emulation for WindowsEmulation {
async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
impl InputEmulation for WindowsEmulation {
async fn consume(&mut self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
rel_mouse(dx as i32, dy as i32);
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
rel_mouse(relative_x as i32, relative_y as i32);
}
PointerEvent::Button {
time: _,
@@ -53,6 +61,7 @@ impl Emulation for WindowsEmulation {
value,
} => scroll(axis, value as i32),
PointerEvent::AxisDiscrete120 { axis, value } => scroll(axis, value),
PointerEvent::Frame {} => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
@@ -70,16 +79,15 @@ impl Emulation for WindowsEmulation {
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
// FIXME
Ok(())
}
async fn create(&mut self, _handle: EmulationHandle) {}
async fn notify(&mut self, _: ClientEvent) {
// nothing to do
}
async fn destroy(&mut self, _handle: EmulationHandle) {}
async fn terminate(&mut self) {}
async fn destroy(&mut self) {}
}
impl WindowsEmulation {

View File

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

View File

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

View File

@@ -0,0 +1,168 @@
use anyhow::Result;
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
ResponseError, Session,
},
WindowIdentifier,
};
use async_trait::async_trait;
use crate::{
client::ClientEvent,
emulate::InputEmulation,
event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
},
};
pub struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a>,
}
impl<'a> DesktopPortalEmulation<'a> {
pub async fn new() -> Result<DesktopPortalEmulation<'a>> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
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?,
};
};
log::debug!("started session");
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event {
Pointer(p) => match p {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if let Err(e) = self
.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await
{
log::warn!("{e}");
}
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
if let Err(e) = self
.proxy
.notify_pointer_axis_discrete(&self.session, axis, value)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
let (dx, dy) = match axis {
Axis::Vertical => (0., value),
Axis::Horizontal => (value, 0.),
};
if let Err(e) = self
.proxy
.notify_pointer_axis(&self.session, dx, dy, true)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Frame {} => {}
},
Keyboard(k) => {
match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await
{
log::warn!("{e}");
}
}
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
_ => {}
}
}
async fn notify(&mut self, _client: ClientEvent) {}
async fn destroy(&mut self) {
log::debug!("closing remote desktop session");
if let Err(e) = self.session.close().await {
log::error!("failed to close remote desktop session: {e}");
}
}
}

View File

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

586
src/event.rs Normal file
View File

@@ -0,0 +1,586 @@
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(),
})),
}
}
}

286
src/frontend.rs Normal file
View File

@@ -0,0 +1,286 @@
use anyhow::{anyhow, Result};
use std::{cmp::min, io::ErrorKind, net::IpAddr, str, time::Duration};
#[cfg(unix)]
use std::{
env,
path::{Path, PathBuf},
};
use tokio::io::ReadHalf;
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
#[cfg(unix)]
use tokio::net::UnixListener;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use serde::{Deserialize, Serialize};
use crate::{
client::{ClientConfig, ClientHandle, ClientState, Position},
config::{Config, Frontend},
};
/// cli frontend
pub mod cli;
/// gtk frontend
#[cfg(feature = "gtk")]
pub mod gtk;
pub fn run_frontend(config: &Config) -> Result<()> {
match config.frontend {
#[cfg(feature = "gtk")]
Frontend::Gtk => {
gtk::run();
}
#[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
cli::run()?;
}
};
Ok(())
}
fn exponential_back_off(duration: &mut Duration) -> &Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
duration
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
pub fn wait_for_service() -> Result<std::os::unix::net::UnixStream> {
let socket_path = FrontendListener::socket_path()?;
let mut duration = Duration::from_millis(1);
loop {
use std::os::unix::net::UnixStream;
if let Ok(stream) = UnixStream::connect(&socket_path) {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
std::thread::sleep(*exponential_back_off(&mut duration));
}
}
#[cfg(windows)]
pub fn wait_for_service() -> Result<std::net::TcpStream> {
let mut duration = Duration::from_millis(1);
loop {
use std::net::TcpStream;
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
break Ok(stream);
}
std::thread::sleep(*exponential_back_off(&mut duration));
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendRequest {
/// activate/deactivate client
Activate(ClientHandle, bool),
/// add a new client
Create,
/// change the listen port (recreate udp listener)
ChangePort(u16),
/// remove a client
Delete(ClientHandle),
/// request an enumeration of all clients
Enumerate(),
/// resolve dns
ResolveDns(ClientHandle),
/// service shutdown
Terminate(),
/// update hostname
UpdateHostname(ClientHandle, Option<String>),
/// update port
UpdatePort(ClientHandle, u16),
/// update position
UpdatePosition(ClientHandle, Position),
/// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>),
/// request the state of the given client
GetState(ClientHandle),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
/// a client was created
Created(ClientHandle, ClientConfig, ClientState),
/// no such client
NoSuchClient(ClientHandle),
/// state changed
State(ClientHandle, ClientConfig, ClientState),
/// the client was deleted
Deleted(ClientHandle),
/// new port, reason of failure (if failed)
PortChanged(u16, Option<String>),
/// list of all clients, used for initial state synchronization
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
/// an error occured
Error(String),
}
pub struct FrontendListener {
#[cfg(windows)]
listener: TcpListener,
#[cfg(unix)]
listener: UnixListener,
#[cfg(unix)]
socket_path: PathBuf,
#[cfg(unix)]
tx_streams: Vec<WriteHalf<UnixStream>>,
#[cfg(windows)]
tx_streams: Vec<WriteHalf<TcpStream>>,
}
impl FrontendListener {
#[cfg(all(unix, not(target_os = "macos")))]
pub fn socket_path() -> Result<PathBuf> {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
};
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn socket_path() -> Result<PathBuf> {
let home = match env::var("HOME") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
};
let home = Path::new(home.as_str());
let path = home
.join("Library")
.join("Caches")
.join("lan-mouse-socket.sock");
Ok(path)
}
pub async fn new() -> Option<Result<Self>> {
#[cfg(unix)]
let (socket_path, listener) = {
let socket_path = match Self::socket_path() {
Ok(path) => path,
Err(e) => return Some(Err(e)),
};
log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() {
// try to connect to see if some other instance
// of lan-mouse is already running
match UnixStream::connect(&socket_path).await {
// connected -> lan-mouse is already running
Ok(_) => return None,
// lan-mouse is not running but a socket was left behind
Err(e) => {
log::debug!("{socket_path:?}: {e} - removing left behind socket");
let _ = std::fs::remove_file(&socket_path);
}
}
}
let listener = match UnixListener::bind(&socket_path) {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
(socket_path, listener)
};
#[cfg(windows)]
let listener = match TcpListener::bind("127.0.0.1:5252").await {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
let adapter = Self {
listener,
#[cfg(unix)]
socket_path,
tx_streams: vec![],
};
Some(Ok(adapter))
}
#[cfg(unix)]
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
Ok(rx)
}
#[cfg(windows)]
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
Ok(rx)
}
pub(crate) async fn broadcast_event(&mut self, notify: FrontendEvent) -> Result<()> {
// encode event
let json = serde_json::to_string(&notify).unwrap();
let payload = json.as_bytes();
let len = payload.len().to_be_bytes();
log::debug!("broadcasting event to streams: {json}");
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if tx.write(&len).await.is_err() {
keep.push(false);
continue;
}
if tx.write(payload).await.is_err() {
keep.push(false);
continue;
}
keep.push(true);
}
// could not find a better solution because async
let mut keep = keep.into_iter();
self.tx_streams.retain(|_| keep.next().unwrap());
Ok(())
}
}
#[cfg(unix)]
impl Drop for FrontendListener {
fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path);
let _ = std::fs::remove_file(&self.socket_path);
}
}
#[cfg(unix)]
pub async fn wait_for_request(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendRequest> {
let len = stream.read_u64().await?;
assert!(len <= 256);
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;
Ok(serde_json::from_slice(&buf[..len as usize])?)
}
#[cfg(windows)]
pub async fn wait_for_request(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendRequest> {
let len = stream.read_u64().await?;
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;
Ok(serde_json::from_slice(&buf[..len as usize])?)
}

View File

@@ -1,65 +1,78 @@
use futures::StreamExt;
use anyhow::{anyhow, Result};
use tokio::{
io::{AsyncBufReadExt, BufReader},
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
task::LocalSet,
};
#[cfg(windows)]
use tokio::net::tcp::{ReadHalf, WriteHalf};
#[cfg(unix)]
use tokio::net::unix::{ReadHalf, WriteHalf};
use std::io::{self, Write};
use crate::{
client::{ClientConfig, ClientHandle, ClientState},
config::DEFAULT_PORT,
};
use self::command::{Command, CommandType};
use lan_mouse_ipc::{
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
};
use super::{FrontendEvent, FrontendRequest};
mod command;
pub fn run() -> Result<(), IpcError> {
pub fn run() -> Result<()> {
let Ok(stream) = super::wait_for_service() else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(async move {
let (rx, tx) = lan_mouse_ipc::connect_async().await?;
stream.set_nonblocking(true)?;
#[cfg(unix)]
let mut stream = tokio::net::UnixStream::from_std(stream)?;
#[cfg(windows)]
let mut stream = tokio::net::TcpStream::from_std(stream)?;
let (rx, tx) = stream.split();
let mut cli = Cli::new(rx, tx);
cli.run().await
}))?;
Ok(())
}
struct Cli {
struct Cli<'a> {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
changed: Option<ClientHandle>,
rx: AsyncFrontendEventReader,
tx: AsyncFrontendRequestWriter,
rx: ReadHalf<'a>,
tx: WriteHalf<'a>,
}
impl Cli {
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
impl<'a> Cli<'a> {
fn new(rx: ReadHalf<'a>, tx: WriteHalf<'a>) -> Cli<'a> {
Self {
clients: vec![],
changed: None,
rx,
tx,
}
}
async fn run(&mut self) -> Result<(), IpcError> {
async fn run(&mut self) -> Result<()> {
let stdin = tokio::io::stdin();
let stdin = BufReader::new(stdin);
let mut stdin = stdin.lines();
/* initial state sync */
let request = FrontendRequest::Enumerate();
self.send_request(request).await?;
self.clients = loop {
match self.rx.next().await {
Some(Ok(e)) => {
if let FrontendEvent::Enumerate(clients) = e {
break clients;
}
}
Some(Err(e)) => return Err(e),
None => return Ok(()),
let event = self.await_event().await?;
if let FrontendEvent::Enumerate(clients) = event {
break clients;
}
};
@@ -79,23 +92,18 @@ impl Cli {
};
self.execute(cmd).await?;
}
event = self.rx.next() => {
if let Some(event) = event {
self.handle_event(event?);
} else {
break Ok(());
}
event = self.await_event() => {
let event = event?;
self.handle_event(event);
}
}
if let Some(handle) = self.changed.take() {
self.update_client(handle).await?;
}
}
}
async fn update_client(&mut self, handle: ClientHandle) -> Result<(), IpcError> {
self.tx.request(FrontendRequest::GetState(handle)).await?;
while let Some(Ok(event)) = self.rx.next().await {
async fn update_client(&mut self, handle: ClientHandle) -> Result<()> {
self.send_request(FrontendRequest::GetState(handle)).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
break;
@@ -104,23 +112,22 @@ impl Cli {
Ok(())
}
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
async fn execute(&mut self, cmd: Command) -> Result<()> {
match cmd {
Command::None => {}
Command::Connect(pos, host, port) => {
let request = FrontendRequest::Create;
self.tx.request(request).await?;
self.send_request(request).await?;
let handle = loop {
if let Some(Ok(event)) = self.rx.next().await {
match event {
FrontendEvent::Created(h, c, s) => {
self.clients.push((h, c, s));
break h;
}
_ => {
self.handle_event(event);
continue;
}
let event = self.await_event().await?;
match event {
FrontendEvent::Created(h, c, s) => {
self.clients.push((h, c, s));
break h;
}
_ => {
self.handle_event(event);
continue;
}
}
};
@@ -129,36 +136,35 @@ impl Cli {
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
FrontendRequest::UpdatePosition(handle, pos),
] {
self.tx.request(request).await?;
self.send_request(request).await?;
}
self.update_client(handle).await?;
}
Command::Disconnect(id) => {
self.tx.request(FrontendRequest::Delete(id)).await?;
self.send_request(FrontendRequest::Delete(id)).await?;
loop {
if let Some(Ok(event)) = self.rx.next().await {
self.handle_event(event.clone());
if let FrontendEvent::Deleted(_) = event {
self.handle_event(event);
break;
}
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Deleted(_) = event {
self.handle_event(event);
break;
}
}
}
Command::Activate(id) => {
self.tx.request(FrontendRequest::Activate(id, true)).await?;
self.send_request(FrontendRequest::Activate(id, true))
.await?;
self.update_client(id).await?;
}
Command::Deactivate(id) => {
self.tx
.request(FrontendRequest::Activate(id, false))
self.send_request(FrontendRequest::Activate(id, false))
.await?;
self.update_client(id).await?;
}
Command::List => {
self.tx.request(FrontendRequest::Enumerate()).await?;
while let Some(e) = self.rx.next().await {
let event = e?;
self.send_request(FrontendRequest::Enumerate()).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Enumerate(_) = event {
break;
@@ -167,12 +173,12 @@ impl Cli {
}
Command::SetHost(handle, host) => {
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
self.tx.request(request).await?;
self.send_request(request).await?;
self.update_client(handle).await?;
}
Command::SetPort(handle, port) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.tx.request(request).await?;
self.send_request(request).await?;
self.update_client(handle).await?;
}
Command::Help => {
@@ -209,7 +215,6 @@ impl Cli {
fn handle_event(&mut self, event: FrontendEvent) {
match event {
FrontendEvent::Changed(h) => self.changed = Some(h),
FrontendEvent::Created(h, c, s) => {
eprint!("client added ({h}): ");
print_config(&c);
@@ -268,12 +273,6 @@ impl Cli {
FrontendEvent::Error(e) => {
eprintln!("ERROR: {e}");
}
FrontendEvent::CaptureStatus(s) => {
eprintln!("capture status: {s:?}")
}
FrontendEvent::EmulationStatus(s) => {
eprintln!("emulation status: {s:?}")
}
}
}
@@ -286,6 +285,23 @@ impl Cli {
eprintln!();
}
}
async fn send_request(&mut self, request: FrontendRequest) -> io::Result<()> {
let json = serde_json::to_string(&request).unwrap();
let bytes = json.as_bytes();
let len = bytes.len();
self.tx.write_u64(len as u64).await?;
self.tx.write_all(bytes).await?;
Ok(())
}
async fn await_event(&mut self) -> Result<FrontendEvent> {
let len = self.rx.read_u64().await?;
let mut buf = vec![0u8; len as usize];
self.rx.read_exact(&mut buf).await?;
let event: FrontendEvent = serde_json::from_slice(&buf)?;
Ok(event)
}
}
fn prompt() -> io::Result<()> {

View File

@@ -3,7 +3,7 @@ use std::{
str::{FromStr, SplitWhitespace},
};
use lan_mouse_ipc::{ClientHandle, Position};
use crate::client::{ClientHandle, Position};
pub(super) enum CommandType {
NoCommand,

159
src/frontend/gtk.rs Normal file
View File

@@ -0,0 +1,159 @@
mod client_object;
mod client_row;
mod window;
use std::{
env,
io::{ErrorKind, Read},
process, str,
};
use crate::frontend::{gtk::window::Window, FrontendRequest};
use adw::Application;
use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use super::FrontendEvent;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
#[cfg(windows)]
let ret = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
.name("gtk".into())
.spawn(gtk_main)
.unwrap()
.join()
.unwrap();
#[cfg(not(windows))]
let ret = gtk_main();
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret
}
fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.LanMouse")
.build();
app.connect_startup(|_| load_icons());
app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![];
app.run_with_args(&args)
}
fn load_icons() {
let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display);
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket");
let mut rx = match super::wait_for_service() {
Ok(stream) => stream,
Err(e) => {
log::error!("could not connect to lan-mouse-socket: {e}");
process::exit(1);
}
};
let tx = match rx.try_clone() {
Ok(sock) => sock,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
log::debug!("connected to lan-mouse-socket");
let (sender, receiver) = async_channel::bounded(10);
gio::spawn_blocking(move || {
match loop {
// read length
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
let len = usize::from_be_bytes(len);
// read payload
let mut buf = vec![0u8; len];
match rx.read_exact(&mut buf) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
// parse json
let json = str::from_utf8(&buf).unwrap();
match serde_json::from_str(json) {
Ok(notify) => sender.send_blocking(notify).unwrap(),
Err(e) => log::error!("{e}"),
}
} {
Ok(()) => {}
Err(e) => log::error!("{e}"),
}
});
let window = Window::new(app, tx);
window.request(FrontendRequest::Enumerate());
glib::spawn_future_local(clone!(@weak window => async move {
loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify {
FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state);
},
FrontendEvent::Deleted(client) => {
window.delete_client(client);
}
FrontendEvent::State(handle, config, state) => {
window.update_client_config(handle, config);
window.update_client_state(handle, state);
}
FrontendEvent::NoSuchClient(_) => { }
FrontendEvent::Error(e) => {
window.show_toast(e.as_str());
},
FrontendEvent::Enumerate(clients) => {
for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else {
window.new_client(handle, client, state);
}
}
},
FrontendEvent::PortChanged(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
}
}
}));
window.present();
}

View File

@@ -3,7 +3,7 @@ mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState};
use crate::client::{ClientConfig, ClientHandle, ClientState};
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);

View File

@@ -5,7 +5,7 @@ use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use lan_mouse_ipc::ClientHandle;
use crate::client::ClientHandle;
use super::ClientData;

View File

@@ -4,7 +4,7 @@ use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use lan_mouse_ipc::DEFAULT_PORT;
use crate::config::DEFAULT_PORT;
use super::ClientObject;

View File

@@ -52,13 +52,10 @@ impl ObjectSubclass for ClientRow {
impl ObjectImpl for ClientRow {
fn constructed(&self) {
self.parent_constructed();
self.delete_button.connect_clicked(clone!(
#[weak(rename_to = row)]
self,
move |button| {
self.delete_button
.connect_clicked(clone!(@weak self as row => move |button| {
row.handle_client_delete(button);
}
));
}));
}
fn signals() -> &'static [glib::subclass::Signal] {

View File

@@ -1,5 +1,13 @@
mod imp;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[cfg(windows)]
use std::net::TcpStream;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
@@ -9,12 +17,13 @@ use gtk::{
ListBox, NoSelection,
};
use lan_mouse_ipc::{
ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
DEFAULT_PORT,
use crate::{
client::{ClientConfig, ClientHandle, ClientState, Position},
config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendRequest},
};
use super::{client_object::ClientObject, client_row::ClientRow};
use super::client_row::ClientRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@@ -24,13 +33,13 @@ glib::wrapper! {
}
impl Window {
pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
pub(crate) fn new(
app: &adw::Application,
#[cfg(unix)] tx: UnixStream,
#[cfg(windows)] tx: TcpStream,
) -> Self {
let window: Self = Object::builder().property("application", app).build();
window
.imp()
.frontend_request_writer
.borrow_mut()
.replace(conn);
window.imp().stream.borrow_mut().replace(tx);
window
}
@@ -53,61 +62,31 @@ impl Window {
let selection_model = NoSelection::new(Some(self.clients()));
self.imp().client_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let client_object = obj
.downcast_ref()
.expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.connect_closure(
"request-update",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, active: bool| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_activate(&client, active);
window.request_client_update(&client);
window.request_client_state(&client);
}
}
),
);
row.connect_closure(
"request-delete",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_delete(&client);
}
}
),
);
row.connect_closure(
"request-dns",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_update(&client);
window.request_dns(&client);
window.request_client_state(&client);
}
}
),
);
row.upcast()
}
),
clone!(@weak self as window => @default-panic, move |obj| {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_activate(&client, active);
window.request_client_update(&client);
window.request_client_state(&client);
}
}));
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_delete(&client);
}
}));
row.connect_closure("request-dns", false, closure_local!(@strong window => move
|row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_update(&client);
window.request_dns(&client);
window.request_client_state(&client);
}
}));
row.upcast()
})
);
}
@@ -227,39 +206,29 @@ impl Window {
}
pub fn request_port_change(&self) {
let port = self
.imp()
.port_entry
.get()
.text()
.as_str()
.parse::<u16>()
.unwrap_or(DEFAULT_PORT);
self.request(FrontendRequest::ChangePort(port));
}
pub fn request_capture(&self) {
self.request(FrontendRequest::EnableCapture);
}
pub fn request_emulation(&self) {
self.request(FrontendRequest::EnableEmulation);
let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = port.as_str().parse::<u16>() {
self.request(FrontendRequest::ChangePort(port));
} else {
self.request(FrontendRequest::ChangePort(DEFAULT_PORT));
}
}
pub fn request_client_state(&self, client: &ClientObject) {
self.request_client_state_for(client.handle());
}
pub fn request_client_state_for(&self, handle: ClientHandle) {
self.request(FrontendRequest::GetState(handle));
let handle = client.handle();
let event = FrontendRequest::GetState(handle);
self.request(event);
}
pub fn request_client_create(&self) {
self.request(FrontendRequest::Create);
let event = FrontendRequest::Create;
self.request(event);
}
pub fn request_dns(&self, client: &ClientObject) {
self.request(FrontendRequest::ResolveDns(client.get_data().handle));
let data = client.get_data();
let event = FrontendRequest::ResolveDns(data.handle);
self.request(event);
}
pub fn request_client_update(&self, client: &ClientObject) {
@@ -279,17 +248,28 @@ impl Window {
}
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
self.request(FrontendRequest::Activate(client.handle(), active));
let handle = client.handle();
let event = FrontendRequest::Activate(handle, active);
self.request(event);
}
pub fn request_client_delete(&self, client: &ClientObject) {
self.request(FrontendRequest::Delete(client.handle()));
let handle = client.handle();
let event = FrontendRequest::Delete(handle);
self.request(event);
}
pub fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) {
pub fn request(&self, event: FrontendRequest) {
let json = serde_json::to_string(&event).unwrap();
log::debug!("requesting: {json}");
let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = stream.write(bytes) {
log::error!("error sending message: {e}");
};
}
@@ -299,24 +279,4 @@ impl Window {
let toast_overlay = &self.imp().toast_overlay;
toast_overlay.add_toast(toast);
}
pub fn set_capture(&self, active: bool) {
self.imp().capture_active.replace(active);
self.update_capture_emulation_status();
}
pub fn set_emulation(&self, active: bool) {
self.imp().emulation_active.replace(active);
self.update_capture_emulation_status();
}
fn update_capture_emulation_status(&self) {
let capture = self.imp().capture_active.get();
let emulation = self.imp().emulation_active.get();
self.imp().capture_status_row.set_visible(!capture);
self.imp().emulation_status_row.set_visible(!emulation);
self.imp()
.capture_emulation_group
.set_visible(!capture || !emulation);
}
}

View File

@@ -1,12 +1,17 @@
use std::cell::{Cell, RefCell};
#[cfg(windows)]
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
use adw::{prelude::*, ActionRow, ToastOverlay};
use glib::subclass::InitializingObject;
use gtk::glib::clone;
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
use crate::config::DEFAULT_PORT;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
@@ -25,21 +30,12 @@ pub struct Window {
pub hostname_label: TemplateChild<Label>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
#[template_child]
pub capture_emulation_group: TemplateChild<PreferencesGroup>,
#[template_child]
pub capture_status_row: TemplateChild<ActionRow>,
#[template_child]
pub emulation_status_row: TemplateChild<ActionRow>,
#[template_child]
pub input_emulation_button: TemplateChild<Button>,
#[template_child]
pub input_capture_button: TemplateChild<Button>,
pub clients: RefCell<Option<gio::ListStore>>,
pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
#[cfg(unix)]
pub stream: RefCell<Option<UnixStream>>,
#[cfg(windows)]
pub stream: RefCell<Option<TcpStream>>,
pub port: Cell<u16>,
pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>,
}
#[glib::object_subclass]
@@ -76,15 +72,11 @@ impl Window {
clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
button.set_icon_name("emblem-ok-symbolic");
button.set_css_classes(&["success"]);
glib::spawn_future_local(clone!(
#[weak]
button,
async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
}
));
glib::spawn_future_local(clone!(@weak button => async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
}));
}
}
@@ -108,16 +100,6 @@ impl Window {
self.port_edit_cancel.set_visible(false);
}
#[template_callback]
fn handle_emulation(&self) {
self.obj().request_emulation();
}
#[template_callback]
fn handle_capture(&self) {
self.obj().request_capture();
}
pub fn set_port(&self, port: u16) {
self.port.set(port);
if port == DEFAULT_PORT {

View File

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

View File

@@ -1,36 +1,10 @@
use env_logger::Env;
use input_capture::InputCaptureError;
use input_emulation::InputEmulationError;
use lan_mouse::{
capture_test,
config::{Config, ConfigError, Frontend},
emulation_test,
server::{Server, ServiceError},
};
use lan_mouse_ipc::IpcError;
use std::{
future::Future,
io,
process::{self, Child, Command},
};
use thiserror::Error;
use tokio::task::LocalSet;
use anyhow::Result;
use std::process::{self, Child, Command};
#[derive(Debug, Error)]
enum LanMouseError {
#[error(transparent)]
Service(#[from] ServiceError),
#[error(transparent)]
IpcError(#[from] IpcError),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Capture(#[from] InputCaptureError),
#[error(transparent)]
Emulation(#[from] InputEmulationError),
}
use env_logger::Env;
use lan_mouse::{capture_test, config::Config, emulation_test, frontend, server::Server};
use tokio::task::LocalSet;
pub fn main() {
// init logging
@@ -43,24 +17,32 @@ pub fn main() {
}
}
fn run() -> Result<(), LanMouseError> {
pub fn start_service() -> Result<Child> {
let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("--daemon")
.spawn()?;
Ok(child)
}
pub fn run() -> Result<()> {
// parse config file + cli args
let config = Config::new()?;
log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind);
if config.test_capture {
run_async(capture_test::run(config))?;
capture_test::run()?;
} else if config.test_emulation {
run_async(emulation_test::run(config))?;
emulation_test::run()?;
} else if config.daemon {
// if daemon is specified we run the service
run_async(run_service(config))?;
run_service(&config)?;
} else {
// otherwise start the service as a child process and
// run a frontend
let mut service = start_service()?;
run_frontend(&config)?;
frontend::run_frontend(&config)?;
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully
@@ -73,14 +55,10 @@ fn run() -> Result<(), LanMouseError> {
service.kill()?;
}
Ok(())
anyhow::Ok(())
}
fn run_async<F, E>(f: F) -> Result<(), LanMouseError>
where
F: Future<Output = Result<(), E>>,
LanMouseError: From<E>,
{
fn run_service(config: &Config) -> Result<()> {
// create single threaded tokio runtime
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
@@ -88,35 +66,15 @@ where
.build()?;
// run async event loop
Ok(runtime.block_on(LocalSet::new().run_until(f))?)
}
runtime.block_on(LocalSet::new().run_until(async {
// run main loop
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
fn start_service() -> Result<Child, io::Error> {
let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("--daemon")
.spawn()?;
Ok(child)
}
let server = Server::new(config);
server.run().await?;
async fn run_service(config: Config) -> Result<(), ServiceError> {
log::info!("Press {:?} to release the mouse", config.release_bind);
Server::new(config).run().await?;
log::info!("service exited!");
Ok(())
}
fn run_frontend(config: &Config) -> Result<(), IpcError> {
match config.frontend {
#[cfg(feature = "gtk")]
Frontend::Gtk => {
lan_mouse_gtk::run();
}
#[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
lan_mouse_cli::run()?;
}
};
log::debug!("service exiting");
anyhow::Ok(())
}))?;
Ok(())
}

View File

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

View File

@@ -1,31 +1,27 @@
use capture_task::CaptureRequest;
use emulation_task::EmulationRequest;
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use local_channel::mpsc::{channel, Sender};
use log;
use std::{
cell::{Cell, RefCell},
collections::{HashSet, VecDeque},
io,
net::{IpAddr, SocketAddr},
collections::HashSet,
rc::Rc,
};
use thiserror::Error;
use tokio::{join, signal, sync::Notify};
use tokio_util::sync::CancellationToken;
use tokio::signal;
use crate::{client::ClientManager, config::Config, dns::DnsResolver};
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
ListenerCreationError, Position, Status,
use crate::{
client::{ClientConfig, ClientHandle, ClientManager, ClientState},
config::Config,
dns,
frontend::{FrontendListener, FrontendRequest},
server::capture_task::CaptureEvent,
};
use self::{emulation_task::EmulationEvent, resolver_task::DnsRequest};
mod capture_task;
mod emulation_task;
mod frontend_task;
mod network_task;
mod ping_task;
mod resolver_task;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
@@ -35,47 +31,22 @@ enum State {
Receiving,
/// Entered the deadzone of another device but waiting
/// for acknowledgement (Leave event) from the device
AwaitAck,
}
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
#[error(transparent)]
Io(#[from] io::Error),
AwaitingLeave,
}
#[derive(Clone)]
pub struct Server {
active_client: Rc<Cell<Option<ClientHandle>>>,
pub(crate) client_manager: Rc<RefCell<ClientManager>>,
client_manager: Rc<RefCell<ClientManager>>,
port: Rc<Cell<u16>>,
state: Rc<Cell<State>>,
release_bind: Vec<input_event::scancode::Linux>,
notifies: Rc<Notifies>,
config: Rc<Config>,
pending_frontend_events: Rc<RefCell<VecDeque<FrontendEvent>>>,
capture_status: Rc<Cell<Status>>,
emulation_status: Rc<Cell<Status>>,
}
#[derive(Default)]
struct Notifies {
capture: Notify,
emulation: Notify,
ping: Notify,
port_changed: Notify,
frontend_event_pending: Notify,
cancel: CancellationToken,
release_bind: Vec<crate::scancode::Linux>,
}
impl Server {
pub fn new(config: Config) -> Self {
pub fn new(config: &Config) -> Self {
let active_client = Rc::new(Cell::new(None));
let client_manager = Rc::new(RefCell::new(ClientManager::default()));
let client_manager = Rc::new(RefCell::new(ClientManager::new()));
let state = Rc::new(Cell::new(State::Receiving));
let port = Rc::new(Cell::new(config.port));
for config_client in config.get_clients() {
@@ -96,467 +67,147 @@ impl Server {
let c = client_manager.get_mut(handle).expect("invalid handle");
*c = (client, state);
}
// task notification tokens
let notifies = Rc::new(Notifies::default());
let release_bind = config.release_bind.clone();
let config = Rc::new(config);
Self {
config,
active_client,
client_manager,
port,
state,
release_bind,
notifies,
pending_frontend_events: Rc::new(RefCell::new(VecDeque::new())),
capture_status: Default::default(),
emulation_status: Default::default(),
}
}
pub async fn run(&mut self) -> Result<(), ServiceError> {
// create frontend communication adapter, exit if already running
let mut frontend = match AsyncFrontendListener::new().await {
Ok(f) => f,
Err(ListenerCreationError::AlreadyRunning) => {
pub async fn run(&self) -> anyhow::Result<()> {
// create frontend communication adapter
let frontend = match FrontendListener::new().await {
Some(f) => f?,
None => {
// none means some other instance is already running
log::info!("service already running, exiting");
return Ok(());
return anyhow::Ok(());
}
e => e?,
};
let (capture_tx, capture_rx) = channel(); /* requests for input capture */
let (emulation_tx, emulation_rx) = channel(); /* emulation requests */
let (udp_recv_tx, udp_recv_rx) = channel(); /* udp receiver */
let (udp_send_tx, udp_send_rx) = channel(); /* udp sender */
let (dns_tx, dns_rx) = channel(); /* dns requests */
let (timer_tx, timer_rx) = tokio::sync::mpsc::channel(1);
let (frontend_notify_tx, frontend_notify_rx) = tokio::sync::mpsc::channel(1);
let network = network_task::new(self.clone(), udp_recv_tx.clone(), udp_send_rx).await?;
let capture = capture_task::new(self.clone(), capture_rx, udp_send_tx.clone());
let emulation =
emulation_task::new(self.clone(), emulation_rx, udp_recv_rx, udp_send_tx.clone());
let resolver = DnsResolver::new(dns_rx)?;
let dns_task = tokio::task::spawn_local(resolver.run(self.clone()));
// udp task
let (mut udp_task, sender_tx, receiver_rx, port_tx) =
network_task::new(self.clone(), frontend_notify_tx.clone()).await?;
// task that pings clients to see if they are responding
let ping = ping_task::new(
// input capture
let (mut capture_task, capture_channel) = capture_task::new(
self.clone(),
udp_send_tx.clone(),
emulation_tx.clone(),
capture_tx.clone(),
sender_tx.clone(),
timer_tx.clone(),
self.release_bind.clone(),
);
for handle in self.active_clients() {
dns_tx.send(handle).expect("channel closed");
// input emulation
let (mut emulation_task, emulate_channel) = emulation_task::new(
self.clone(),
receiver_rx,
sender_tx.clone(),
capture_channel.clone(),
timer_tx,
);
// create dns resolver
let resolver = dns::DnsResolver::new().await?;
let (mut resolver_task, resolve_tx) =
resolver_task::new(resolver, self.clone(), frontend_notify_tx);
// frontend listener
let (mut frontend_task, frontend_tx) = frontend_task::new(
frontend,
frontend_notify_rx,
self.clone(),
capture_channel.clone(),
emulate_channel.clone(),
resolve_tx.clone(),
port_tx,
);
// task that pings clients to see if they are responding
let mut ping_task = ping_task::new(
self.clone(),
sender_tx.clone(),
emulate_channel.clone(),
capture_channel.clone(),
timer_rx,
);
let active = self
.client_manager
.borrow()
.get_client_states()
.filter_map(|(h, (c, s))| {
if s.active {
Some((h, c.hostname.clone()))
} else {
None
}
})
.collect::<Vec<_>>();
for (handle, hostname) in active {
frontend_tx
.send(FrontendRequest::Activate(handle, true))
.await?;
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
log::info!("running service");
tokio::select! {
_ = signal::ctrl_c() => {
log::info!("terminating service");
}
e = &mut capture_task => {
if let Ok(Err(e)) = e {
log::error!("error in input capture task: {e}");
}
}
e = &mut emulation_task => {
if let Ok(Err(e)) = e {
log::error!("error in input emulation task: {e}");
}
}
e = &mut frontend_task => {
if let Ok(Err(e)) = e {
log::error!("error in frontend listener: {e}");
}
}
_ = &mut resolver_task => { }
_ = &mut udp_task => { }
_ = &mut ping_task => { }
}
loop {
tokio::select! {
request = frontend.next() => {
let request = match request {
Some(Ok(r)) => r,
Some(Err(e)) => {
log::error!("error receiving request: {e}");
continue;
}
None => break,
};
log::debug!("handle frontend request: {request:?}");
self.handle_request(&capture_tx.clone(), &emulation_tx.clone(), request, &dns_tx);
}
_ = self.notifies.frontend_event_pending.notified() => {
while let Some(event) = {
/* need to drop borrow before next iteration! */
let event = self.pending_frontend_events.borrow_mut().pop_front();
event
} {
frontend.broadcast(event).await;
}
},
_ = self.cancelled() => break,
r = signal::ctrl_c() => {
r.expect("failed to wait for CTRL+C");
break;
}
let _ = emulate_channel.send(EmulationEvent::Terminate).await;
let _ = capture_channel.send(CaptureEvent::Terminate).await;
let _ = frontend_tx.send(FrontendRequest::Terminate()).await;
if !capture_task.is_finished() {
if let Err(e) = capture_task.await {
log::error!("error in input capture task: {e}");
}
}
if !emulation_task.is_finished() {
if let Err(e) = emulation_task.await {
log::error!("error in input emulation task: {e}");
}
}
log::info!("terminating service");
if !frontend_task.is_finished() {
if let Err(e) = frontend_task.await {
log::error!("error in frontend listener: {e}");
}
}
self.cancel();
let _ = join!(capture, dns_task, emulation, network, ping);
resolver_task.abort();
udp_task.abort();
ping_task.abort();
Ok(())
}
fn notify_frontend(&self, event: FrontendEvent) {
self.pending_frontend_events.borrow_mut().push_back(event);
self.notifies.frontend_event_pending.notify_one();
}
fn cancel(&self) {
self.notifies.cancel.cancel();
}
pub(crate) async fn cancelled(&self) {
self.notifies.cancel.cancelled().await
}
fn is_cancelled(&self) -> bool {
self.notifies.cancel.is_cancelled()
}
fn notify_capture(&self) {
log::info!("received capture enable request");
self.notifies.capture.notify_waiters()
}
async fn capture_enabled(&self) {
self.notifies.capture.notified().await
}
fn notify_emulation(&self) {
log::info!("received emulation enable request");
self.notifies.emulation.notify_waiters()
}
async fn emulation_notified(&self) {
self.notifies.emulation.notified().await
}
fn restart_ping_timer(&self) {
self.notifies.ping.notify_waiters()
}
async fn ping_timer_notified(&self) {
self.notifies.ping.notified().await
}
fn request_port_change(&self, port: u16) {
self.port.replace(port);
self.notifies.port_changed.notify_one();
}
fn notify_port_changed(&self, port: u16, msg: Option<String>) {
self.port.replace(port);
self.notify_frontend(FrontendEvent::PortChanged(port, msg));
}
pub(crate) fn client_updated(&self, handle: ClientHandle) {
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn active_clients(&self) -> Vec<ClientHandle> {
self.client_manager
.borrow()
.get_client_states()
.filter(|(_, (_, s))| s.active)
.map(|(h, _)| h)
.collect()
}
fn handle_request(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
event: FrontendRequest,
dns: &Sender<ClientHandle>,
) -> bool {
log::debug!("frontend: {event:?}");
match event {
FrontendRequest::EnableCapture => self.notify_capture(),
FrontendRequest::EnableEmulation => self.notify_emulation(),
FrontendRequest::Create => {
self.add_client();
}
FrontendRequest::Activate(handle, active) => {
if active {
self.activate_client(capture, emulate, handle);
} else {
self.deactivate_client(capture, emulate, handle);
}
}
FrontendRequest::ChangePort(port) => self.request_port_change(port),
FrontendRequest::Delete(handle) => {
self.remove_client(capture, emulate, handle);
self.notify_frontend(FrontendEvent::Deleted(handle));
}
FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
FrontendRequest::UpdateHostname(handle, host) => {
self.update_hostname(handle, host, dns)
}
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, capture, emulate, pos)
}
FrontendRequest::ResolveDns(handle) => dns.send(handle).expect("channel closed"),
FrontendRequest::Sync => {
self.enumerate();
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status.get()));
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status.get()));
self.notify_frontend(FrontendEvent::PortChanged(self.port.get(), None));
}
};
false
}
fn enumerate(&self) {
let clients = self
.client_manager
.borrow()
.get_client_states()
.map(|(h, (c, s))| (h, c.clone(), s.clone()))
.collect();
self.notify_frontend(FrontendEvent::Enumerate(clients));
}
fn add_client(&self) -> ClientHandle {
let handle = self.client_manager.borrow_mut().add_client();
log::info!("added client {handle}");
let (c, s) = self.client_manager.borrow().get(handle).unwrap().clone();
self.notify_frontend(FrontendEvent::Created(handle, c, s));
handle
}
fn deactivate_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
match self.client_manager.borrow_mut().get_mut(handle) {
None => return,
Some((_, s)) if !s.active => return,
Some((_, s)) => s.active = false,
};
let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle));
self.client_updated(handle);
log::info!("deactivated client {handle}");
}
fn activate_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
/* deactivate potential other client at this position */
let pos = match self.client_manager.borrow().get(handle) {
None => return,
Some((_, s)) if s.active => return,
Some((client, _)) => client.pos,
};
let other = self.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other {
self.deactivate_client(capture, emulate, other);
}
/* activate the client */
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.active = true;
} else {
return;
};
/* notify emulation, capture and frontends */
let _ = capture.send(CaptureRequest::Create(handle, to_capture_pos(pos)));
let _ = emulate.send(EmulationRequest::Create(handle));
self.client_updated(handle);
log::info!("activated client {handle} ({pos})");
}
fn remove_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
let Some(active) = self
.client_manager
.borrow_mut()
.remove_client(handle)
.map(|(_, s)| s.active)
else {
return;
};
if active {
let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle));
}
}
fn update_pressed_keys(&self, handle: ClientHandle, has_pressed_keys: bool) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.has_pressed_keys = has_pressed_keys;
}
}
fn update_fix_ips(&self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
if let Some((c, _)) = self.client_manager.borrow_mut().get_mut(handle) {
c.fix_ips = fix_ips;
};
self.update_ips(handle);
self.client_updated(handle);
}
pub(crate) fn update_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.dns_ips = dns_ips;
};
self.update_ips(handle);
self.client_updated(handle);
}
fn update_ips(&self, handle: ClientHandle) {
if let Some((c, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.ips = c
.fix_ips
.iter()
.cloned()
.chain(s.dns_ips.iter().cloned())
.collect::<HashSet<_>>();
}
}
fn update_hostname(
&self,
handle: ClientHandle,
hostname: Option<String>,
dns: &Sender<ClientHandle>,
) {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
// hostname changed
if c.hostname != hostname {
c.hostname = hostname;
s.active_addr = None;
s.dns_ips.clear();
drop(client_manager);
self.update_ips(handle);
dns.send(handle).expect("channel closed");
}
self.client_updated(handle);
}
fn update_port(&self, handle: ClientHandle, port: u16) {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
if c.port != port {
c.port = port;
s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
}
fn update_pos(
&self,
handle: ClientHandle,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
pos: Position,
) {
let (changed, active) = {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
let changed = c.pos != pos;
if changed {
log::info!("update pos {handle} {} -> {}", c.pos, pos);
}
c.pos = pos;
(changed, s.active)
};
// update state in event input emulator & input capture
if changed {
self.deactivate_client(capture, emulate, handle);
if active {
self.activate_client(capture, emulate, handle);
}
}
}
fn broadcast_client(&self, handle: ClientHandle) {
let client = self.client_manager.borrow().get(handle).cloned();
let event = if let Some((config, state)) = client {
FrontendEvent::State(handle, config, state)
} else {
FrontendEvent::NoSuchClient(handle)
};
self.notify_frontend(event);
}
fn set_emulation_status(&self, status: Status) {
self.emulation_status.replace(status);
let status = FrontendEvent::EmulationStatus(status);
self.notify_frontend(status);
}
fn set_capture_status(&self, status: Status) {
self.capture_status.replace(status);
let status = FrontendEvent::CaptureStatus(status);
self.notify_frontend(status);
}
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.resolving = status;
}
self.client_updated(handle);
}
pub(crate) fn get_hostname(&self, handle: ClientHandle) -> Option<String> {
self.client_manager
.borrow_mut()
.get_mut(handle)
.and_then(|(c, _)| c.hostname.clone())
}
fn get_state(&self) -> State {
self.state.get()
}
fn set_state(&self, state: State) {
log::debug!("state => {state:?}");
self.state.replace(state);
}
fn set_active(&self, handle: Option<ClientHandle>) {
log::debug!("active client => {handle:?}");
self.active_client.replace(handle);
}
fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
self.client_manager
.borrow()
.get(handle)
.and_then(|(_, s)| s.active_addr)
}
}
fn to_capture_pos(pos: Position) -> input_capture::Position {
match pos {
Position::Left => input_capture::Position::Left,
Position::Right => input_capture::Position::Right,
Position::Top => input_capture::Position::Top,
Position::Bottom => input_capture::Position::Bottom,
}
}

View File

@@ -1,167 +1,154 @@
use anyhow::{anyhow, Result};
use futures::StreamExt;
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{Receiver, Sender};
use std::net::SocketAddr;
use std::{collections::HashSet, net::SocketAddr};
use tokio::{process::Command, task::JoinHandle};
use tokio::{process::Command, sync::mpsc::Sender, task::JoinHandle};
use input_capture::{
self, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
use crate::{
capture::{self, InputCapture},
client::{ClientEvent, ClientHandle},
event::{Event, KeyboardEvent},
scancode,
server::State,
};
use crate::server::State;
use lan_mouse_ipc::{ClientHandle, Status};
use super::Server;
#[derive(Clone, Copy, Debug)]
pub(crate) enum CaptureRequest {
pub enum CaptureEvent {
/// capture must release the mouse
Release,
/// add a capture client
Create(CaptureHandle, Position),
/// destory a capture client
Destroy(CaptureHandle),
/// capture is notified of a change in client states
ClientEvent(ClientEvent),
/// termination signal
Terminate,
}
pub(crate) fn new(
pub fn new(
server: Server,
capture_rx: Receiver<CaptureRequest>,
udp_send: Sender<(ProtoEvent, SocketAddr)>,
) -> JoinHandle<()> {
let backend = server.config.capture_backend.map(|b| b.into());
tokio::task::spawn_local(capture_task(server, backend, udp_send, capture_rx))
}
async fn capture_task(
server: Server,
backend: Option<input_capture::Backend>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
mut notify_rx: Receiver<CaptureRequest>,
) {
loop {
if let Err(e) = do_capture(backend, &server, &sender_tx, &mut notify_rx).await {
log::warn!("input capture exited: {e}");
}
server.set_capture_status(Status::Disabled);
if server.is_cancelled() {
break;
}
// allow cancellation
sender_tx: Sender<(Event, SocketAddr)>,
timer_tx: Sender<()>,
release_bind: Vec<scancode::Linux>,
) -> (JoinHandle<Result<()>>, Sender<CaptureEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let task = tokio::task::spawn_local(async move {
let mut capture = capture::create().await;
let mut pressed_keys = HashSet::new();
loop {
tokio::select! {
_ = notify_rx.recv() => continue, /* need to ignore requests here! */
_ = server.capture_enabled() => break,
_ = server.cancelled() => return,
}
}
}
}
event = capture.next() => {
match event {
Some(Ok(event)) => handle_capture_event(&server, &mut capture, &sender_tx, &timer_tx, event, &mut pressed_keys, &release_bind).await?,
Some(Err(e)) => return Err(anyhow!("input capture: {e:?}")),
None => return Err(anyhow!("input capture terminated")),
}
}
e = rx.recv() => {
log::debug!("input capture notify rx: {e:?}");
match e {
Some(e) => match e {
CaptureEvent::Release => {
capture.release()?;
server.state.replace(State::Receiving);
async fn do_capture(
backend: Option<input_capture::Backend>,
server: &Server,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
notify_rx: &mut Receiver<CaptureRequest>,
) -> Result<(), InputCaptureError> {
/* allow cancelling capture request */
let mut capture = tokio::select! {
r = InputCapture::new(backend) => r?,
_ = server.cancelled() => return Ok(()),
};
server.set_capture_status(Status::Enabled);
let clients = server.active_clients();
let clients = clients.iter().copied().map(|handle| {
(
handle,
server
.client_manager
.borrow()
.get(handle)
.map(|(c, _)| c.pos)
.expect("no such client"),
)
});
for (handle, pos) in clients {
capture.create(handle, to_capture_pos(pos)).await?;
}
loop {
tokio::select! {
event = capture.next() => match event {
Some(event) => handle_capture_event(server, &mut capture, sender_tx, event?).await?,
None => return Ok(()),
},
e = notify_rx.recv() => {
log::debug!("input capture notify rx: {e:?}");
match e {
Some(e) => match e {
CaptureRequest::Release => {
capture.release().await?;
server.state.replace(State::Receiving);
}
CaptureRequest::Create(h, p) => capture.create(h, p).await?,
CaptureRequest::Destroy(h) => capture.destroy(h).await?,
},
None => break,
}
CaptureEvent::ClientEvent(e) => capture.notify(e)?,
CaptureEvent::Terminate => break,
},
None => break,
}
}
}
_ = server.cancelled() => break,
}
anyhow::Ok(())
});
(task, tx)
}
fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) {
if let Ok(scancode) = scancode::Linux::try_from(key) {
log::debug!("key: {key}, state: {state}, scancode: {scancode:?}");
match state {
1 => pressed_keys.insert(scancode),
_ => pressed_keys.remove(&scancode),
};
}
capture.terminate().await?;
Ok(())
}
async fn handle_capture_event(
server: &Server,
capture: &mut InputCapture,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
event: (CaptureHandle, CaptureEvent),
) -> Result<(), CaptureError> {
let (handle, event) = event;
log::trace!("({handle}) {event:?}");
capture: &mut Box<dyn InputCapture>,
sender_tx: &Sender<(Event, SocketAddr)>,
timer_tx: &Sender<()>,
event: (ClientHandle, Event),
pressed_keys: &mut HashSet<scancode::Linux>,
release_bind: &[scancode::Linux],
) -> Result<()> {
let (handle, mut e) = event;
log::trace!("({handle}) {e:?}");
// capture started
if event == CaptureEvent::Begin {
// wait for remote to acknowlegde enter
server.set_state(State::AwaitAck);
server.set_active(Some(handle));
// restart ping timer to release capture if unreachable
server.restart_ping_timer();
// spawn enter hook cmd
if let Event::Keyboard(KeyboardEvent::Key { key, state, .. }) = e {
update_pressed_keys(pressed_keys, key, state);
log::debug!("{pressed_keys:?}");
if release_bind.iter().all(|k| pressed_keys.contains(k)) {
pressed_keys.clear();
log::info!("releasing pointer");
capture.release()?;
server.state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
// send an event to release all the modifiers
e = Event::Disconnect();
}
}
let (addr, enter, start_timer) = {
let mut enter = false;
let mut start_timer = false;
// get client state for handle
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some((_, s)) => s,
None => {
// should not happen
log::warn!("unknown client!");
capture.release()?;
server.state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
return Ok(());
}
};
// if we just entered the client we want to send additional enter events until
// we get a leave event
if let Event::Enter() = e {
server.state.replace(State::AwaitingLeave);
server.active_client.replace(Some(handle));
log::trace!("Active client => {}", handle);
start_timer = true;
log::trace!("STATE ===> AwaitingLeave");
enter = true;
} else {
// ignore any potential events in receiving mode
if server.state.get() == State::Receiving && e != Event::Disconnect() {
return Ok(());
}
}
(client_state.active_addr, enter, start_timer)
};
if start_timer {
let _ = timer_tx.try_send(());
}
if enter {
spawn_hook_command(server, handle);
}
// release capture if emulation set state to Receiveing
if server.get_state() == State::Receiving {
capture.release().await?;
return Ok(());
if let Some(addr) = addr {
if enter {
let _ = sender_tx.send((Event::Enter(), addr)).await;
}
let _ = sender_tx.send((e, addr)).await;
}
// check release bind
if capture.keys_pressed(&server.release_bind) {
capture.release().await?;
server.set_state(State::Receiving);
}
if let Some(addr) = server.active_addr(handle) {
let event = match server.get_state() {
State::Sending => match event {
CaptureEvent::Begin => ProtoEvent::Enter(0),
CaptureEvent::Input(e) => ProtoEvent::Input(e),
},
/* send additional enter events until acknowleged */
State::AwaitAck => ProtoEvent::Enter(0),
/* released capture */
State::Receiving => ProtoEvent::Leave(0),
};
sender_tx.send((event, addr)).expect("sender closed");
};
Ok(())
}
@@ -195,12 +182,3 @@ fn spawn_hook_command(server: &Server, handle: ClientHandle) {
}
});
}
fn to_capture_pos(pos: lan_mouse_ipc::Position) -> input_capture::Position {
match pos {
lan_mouse_ipc::Position::Left => input_capture::Position::Left,
lan_mouse_ipc::Position::Right => input_capture::Position::Right,
lan_mouse_ipc::Position::Top => input_capture::Position::Top,
lan_mouse_ipc::Position::Bottom => input_capture::Position::Bottom,
}
}

View File

@@ -1,188 +1,239 @@
use local_channel::mpsc::{Receiver, Sender};
use anyhow::{anyhow, Result};
use std::net::SocketAddr;
use lan_mouse_proto::ProtoEvent;
use tokio::task::JoinHandle;
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use lan_mouse_ipc::ClientHandle;
use crate::{
client::{ClientEvent, ClientHandle},
emulate::{self, InputEmulation},
event::{Event, KeyboardEvent},
scancode,
server::State,
};
use crate::{client::ClientManager, server::State};
use input_emulation::{self, EmulationError, EmulationHandle, InputEmulation, InputEmulationError};
use lan_mouse_ipc::Status;
use super::{network_task::NetworkError, Server};
use super::{CaptureEvent, Server};
#[derive(Clone, Debug)]
pub(crate) enum EmulationRequest {
/// create a new client
Create(EmulationHandle),
/// destroy a client
Destroy(EmulationHandle),
pub enum EmulationEvent {
/// input emulation is notified of a change in client states
ClientEvent(ClientEvent),
/// input emulation must release keys for client
ReleaseKeys(ClientHandle),
/// termination signal
Terminate,
}
pub(crate) fn new(
pub fn new(
server: Server,
emulation_rx: Receiver<EmulationRequest>,
udp_rx: Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
) -> JoinHandle<()> {
let emulation_task = emulation_task(server, emulation_rx, udp_rx, sender_tx);
tokio::task::spawn_local(emulation_task)
}
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
sender_tx: Sender<(Event, SocketAddr)>,
capture_tx: Sender<CaptureEvent>,
timer_tx: Sender<()>,
) -> (JoinHandle<Result<()>>, Sender<EmulationEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let emulate_task = tokio::task::spawn_local(async move {
let mut emulate = emulate::create().await;
let mut last_ignored = None;
async fn emulation_task(
server: Server,
mut rx: Receiver<EmulationRequest>,
mut udp_rx: Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
) {
loop {
if let Err(e) = do_emulation(&server, &mut rx, &mut udp_rx, &sender_tx).await {
log::warn!("input emulation exited: {e}");
}
server.set_emulation_status(Status::Disabled);
if server.is_cancelled() {
break;
}
// allow cancellation
loop {
tokio::select! {
_ = rx.recv() => continue, /* need to ignore requests here! */
_ = server.emulation_notified() => break,
_ = server.cancelled() => return,
}
}
}
}
async fn do_emulation(
server: &Server,
rx: &mut Receiver<EmulationRequest>,
udp_rx: &mut Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), InputEmulationError> {
let backend = server.config.emulation_backend.map(|b| b.into());
log::info!("creating input emulation...");
let mut emulation = tokio::select! {
r = InputEmulation::new(backend) => r?,
_ = server.cancelled() => return Ok(()),
};
server.set_emulation_status(Status::Enabled);
// add clients
for handle in server.active_clients() {
emulation.create(handle).await;
}
let res = do_emulation_session(server, &mut emulation, rx, udp_rx, sender_tx).await;
emulation.terminate().await; // manual drop
res
}
async fn do_emulation_session(
server: &Server,
emulation: &mut InputEmulation,
rx: &mut Receiver<EmulationRequest>,
udp_rx: &mut Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), InputEmulationError> {
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = udp_rx.recv() => {
let udp_event = match udp_event.expect("channel closed") {
Ok(e) => e,
Err(e) => {
log::warn!("network error: {e}");
continue;
udp_event = udp_rx.recv() => {
let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??;
handle_udp_rx(&server, &capture_tx, &mut emulate, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await;
}
emulate_event = rx.recv() => {
match emulate_event {
Some(e) => match e {
EmulationEvent::ClientEvent(e) => emulate.notify(e).await,
EmulationEvent::ReleaseKeys(c) => release_keys(&server, &mut emulate, c).await,
EmulationEvent::Terminate => break,
},
None => break,
}
};
handle_incoming_event(server, emulation, sender_tx, &mut last_ignored, udp_event).await?;
}
emulate_event = rx.recv() => {
match emulate_event.expect("channel closed") {
EmulationRequest::Create(h) => { let _ = emulation.create(h).await; },
EmulationRequest::Destroy(h) => emulation.destroy(h).await,
EmulationRequest::ReleaseKeys(c) => emulation.release_keys(c).await?,
}
res = emulate.dispatch() => {
res?;
}
}
_ = server.notifies.cancel.cancelled() => break Ok(()),
}
}
// release potentially still pressed keys
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|(h, _)| h)
.collect::<Vec<_>>();
for client in clients {
release_keys(&server, &mut emulate, client).await;
}
// destroy emulator
emulate.destroy().await;
anyhow::Ok(())
});
(emulate_task, tx)
}
async fn handle_incoming_event(
async fn handle_udp_rx(
server: &Server,
emulate: &mut InputEmulation,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
capture_tx: &Sender<CaptureEvent>,
emulate: &mut Box<dyn InputEmulation>,
sender_tx: &Sender<(Event, SocketAddr)>,
last_ignored: &mut Option<SocketAddr>,
event: (ProtoEvent, SocketAddr),
) -> Result<(), EmulationError> {
event: (Event, SocketAddr),
timer_tx: &Sender<()>,
) {
let (event, addr) = event;
log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
// get client handle for addr
let Some(handle) =
activate_client_if_exists(&mut server.client_manager.borrow_mut(), addr, last_ignored)
else {
return Ok(());
};
match (event, addr) {
(ProtoEvent::Pong, _) => { /* ignore pong events */ }
(ProtoEvent::Ping, addr) => {
let _ = sender_tx.send((ProtoEvent::Pong, addr));
}
(ProtoEvent::Leave(_), _) => emulate.release_keys(handle).await?,
(ProtoEvent::Ack(_), _) => server.set_state(State::Sending),
(ProtoEvent::Enter(_), _) => {
server.set_state(State::Receiving);
sender_tx
.send((ProtoEvent::Ack(0), addr))
.expect("no channel")
}
(ProtoEvent::Input(e), _) => {
if let State::Receiving = server.get_state() {
log::trace!("{event} => emulate");
emulate.consume(e, handle).await?;
let has_pressed_keys = emulate.has_pressed_keys(handle);
server.update_pressed_keys(handle, has_pressed_keys);
if has_pressed_keys {
server.restart_ping_timer();
}
// get handle for addr
let handle = match server.client_manager.borrow().get_client(addr) {
Some(a) => a,
None => {
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;
}
}
Ok(())
}
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)?;
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
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
Some(handle)
// 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) {
(Event::Pong(), _) => { /* ignore pong events */ }
(Event::Ping(), addr) => {
let _ = sender_tx.send((Event::Pong(), addr)).await;
}
(Event::Disconnect(), _) => {
release_keys(server, emulate, handle).await;
}
(event, addr) => {
// tell clients that we are ready to receive events
if let Event::Enter() = event {
let _ = sender_tx.send((Event::Leave(), addr)).await;
}
match server.state.get() {
State::Sending => {
if let Event::Leave() = event {
// ignore additional leave events that may
// have been sent for redundancy
} else {
// upon receiving any event, we go back to receiving mode
server.state.replace(State::Receiving);
let _ = capture_tx.send(CaptureEvent::Release).await;
log::trace!("STATE ===> Receiving");
}
}
State::Receiving => {
let mut ignore_event = false;
if let Event::Keyboard(KeyboardEvent::Key {
time: _,
key,
state,
}) = event
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state = if let Some((_, s)) = client_manager.get_mut(handle) {
s
} else {
log::error!("unknown handle");
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.
if !ignore_event {
// consume event
emulate.consume(event, handle).await;
log::trace!("{event} => emulate");
}
}
State::AwaitingLeave => {
// we just entered the deadzone of a client, so
// we need to ignore events that may still
// be on the way until a leave event occurs
// telling us the client registered the enter
if let Event::Leave() = event {
server.state.replace(State::Sending);
log::trace!("STATE ===> Sending");
}
// entering a client that is waiting for a leave
// event should still be possible
if let Event::Enter() = event {
server.state.replace(State::Receiving);
let _ = capture_tx.send(CaptureEvent::Release).await;
log::trace!("STATE ===> Receiving");
}
}
}
}
}
}
async fn release_keys(
server: &Server,
emulate: &mut Box<dyn InputEmulation>,
client: ClientHandle,
) {
let keys = server
.client_manager
.borrow_mut()
.get_mut(client)
.iter_mut()
.flat_map(|(_, s)| s.pressed_keys.drain())
.collect::<Vec<_>>();
for key in keys {
let event = Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state: 0,
});
emulate.consume(event, client).await;
if let Ok(key) = scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}");
}
}
let modifiers_event = KeyboardEvent::Modifiers {
mods_depressed: 0,
mods_latched: 0,
mods_locked: 0,
group: 0,
};
emulate
.consume(Event::Keyboard(modifiers_event), client)
.await;
}

354
src/server/frontend_task.rs Normal file
View File

@@ -0,0 +1,354 @@
use std::{
collections::HashSet,
io::ErrorKind,
net::{IpAddr, SocketAddr},
};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
use anyhow::{anyhow, Result};
use tokio::{
io::ReadHalf,
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{
client::{ClientEvent, ClientHandle, Position},
frontend::{self, FrontendEvent, FrontendListener, FrontendRequest},
};
use super::{
capture_task::CaptureEvent, emulation_task::EmulationEvent, resolver_task::DnsRequest, Server,
};
pub(crate) fn new(
mut frontend: FrontendListener,
mut notify_rx: Receiver<FrontendEvent>,
server: Server,
capture: Sender<CaptureEvent>,
emulate: Sender<EmulationEvent>,
resolve_ch: Sender<DnsRequest>,
port_tx: Sender<u16>,
) -> (JoinHandle<Result<()>>, Sender<FrontendRequest>) {
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32);
let event_tx_clone = event_tx.clone();
let frontend_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
stream = frontend.accept() => {
let stream = match stream {
Ok(s) => s,
Err(e) => {
log::warn!("error accepting frontend connection: {e}");
continue;
}
};
handle_frontend_stream(&event_tx_clone, stream).await;
}
event = event_rx.recv() => {
let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?;
if handle_frontend_event(&server, &capture, &emulate, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
break;
}
}
notify = notify_rx.recv() => {
let notify = notify.ok_or(anyhow!("frontend notify closed"))?;
let _ = frontend.broadcast_event(notify).await;
}
}
}
anyhow::Ok(())
});
(frontend_task, event_tx)
}
async fn handle_frontend_stream(
frontend_tx: &Sender<FrontendRequest>,
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
) {
use std::io;
let tx = frontend_tx.clone();
tokio::task::spawn_local(async move {
loop {
let request = frontend::wait_for_request(&mut stream).await;
match request {
Ok(request) => {
let _ = tx.send(request).await;
}
Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() {
if e.kind() == ErrorKind::UnexpectedEof {
return;
}
}
log::error!("error reading frontend event: {e}");
return;
}
}
}
});
}
async fn handle_frontend_event(
server: &Server,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>,
frontend: &mut FrontendListener,
port_tx: &Sender<u16>,
event: FrontendRequest,
) -> bool {
log::debug!("frontend: {event:?}");
match event {
FrontendRequest::Create => {
add_client(server, frontend).await;
}
FrontendRequest::Activate(handle, active) => {
if active {
activate_client(server, capture, emulate, handle).await;
} else {
deactivate_client(server, capture, emulate, handle).await;
}
}
FrontendRequest::ChangePort(port) => {
let _ = port_tx.send(port).await;
}
FrontendRequest::Delete(handle) => {
remove_client(server, capture, emulate, handle).await;
broadcast(frontend, FrontendEvent::Deleted(handle)).await;
}
FrontendRequest::Enumerate() => {
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|(h, (c, s))| (h, c.clone(), s.clone()))
.collect();
broadcast(frontend, FrontendEvent::Enumerate(clients)).await;
}
FrontendRequest::GetState(handle) => {
broadcast_client(server, frontend, handle).await;
}
FrontendRequest::Terminate() => {
log::info!("terminating gracefully...");
return true;
}
FrontendRequest::UpdateFixIps(handle, fix_ips) => {
update_fix_ips(server, resolve_tx, handle, fix_ips).await;
}
FrontendRequest::UpdateHostname(handle, hostname) => {
update_hostname(server, resolve_tx, handle, hostname).await;
}
FrontendRequest::UpdatePort(handle, port) => {
update_port(server, handle, port).await;
}
FrontendRequest::UpdatePosition(handle, pos) => {
update_pos(server, handle, capture, emulate, pos).await;
}
FrontendRequest::ResolveDns(handle) => {
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, handle }).await;
}
}
};
false
}
async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) {
if let Err(e) = frontend.broadcast_event(event).await {
log::error!("error notifying frontend: {e}");
}
}
pub async fn add_client(server: &Server, frontend: &mut FrontendListener) {
let handle = server.client_manager.borrow_mut().add_client();
log::info!("added client {handle}");
let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone();
broadcast(frontend, FrontendEvent::Created(handle, c, s)).await;
}
pub async fn deactivate_client(
server: &Server,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
match server.client_manager.borrow_mut().get_mut(handle) {
Some((_, s)) => {
s.active = false;
}
None => return,
};
let event = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::ClientEvent(event)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(event)).await;
}
pub async fn activate_client(
server: &Server,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
/* deactivate potential other client at this position */
let pos = match server.client_manager.borrow().get(handle) {
Some((client, _)) => client.pos,
None => return,
};
let other = server.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other {
if other != handle {
deactivate_client(server, capture, emulate, other).await;
}
}
/* activate the client */
if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) {
s.active = true;
} else {
return;
};
/* notify emulation, capture and frontends */
let event = ClientEvent::Create(handle, pos);
let _ = capture.send(CaptureEvent::ClientEvent(event)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(event)).await;
}
pub async fn remove_client(
server: &Server,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
let Some(active) = server
.client_manager
.borrow_mut()
.remove_client(handle)
.map(|(_, s)| s.active)
else {
return;
};
if active {
let destroy = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
}
}
async fn update_fix_ips(
server: &Server,
resolve_tx: &Sender<DnsRequest>,
handle: ClientHandle,
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 {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
async fn update_hostname(
server: &Server,
resolve_tx: &Sender<DnsRequest>,
handle: ClientHandle,
hostname: Option<String>,
) {
let hostname = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
// update hostname
if c.hostname != hostname {
c.hostname = hostname;
s.ips = HashSet::from_iter(c.fix_ips.iter().cloned());
s.active_addr = None;
c.hostname.clone()
} else {
None
}
};
// resolve to update ips in state
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
async fn update_port(server: &Server, handle: ClientHandle, port: u16) {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
if c.port != port {
c.port = port;
s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
}
async fn update_pos(
server: &Server,
handle: ClientHandle,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
pos: Position,
) {
let (changed, active) = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
let changed = c.pos != pos;
c.pos = pos;
(changed, s.active)
};
// update state in event input emulator & input capture
if changed {
if active {
let destroy = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
}
let create = ClientEvent::Create(handle, pos);
let _ = capture.send(CaptureEvent::ClientEvent(create)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(create)).await;
}
}
async fn broadcast_client(server: &Server, frontend: &mut FrontendListener, handle: ClientHandle) {
let client = server.client_manager.borrow().get(handle).cloned();
if let Some((config, state)) = client {
broadcast(frontend, FrontendEvent::State(handle, config, state)).await;
} else {
broadcast(frontend, FrontendEvent::NoSuchClient(handle)).await;
}
}

View File

@@ -1,99 +1,89 @@
use local_channel::mpsc::{Receiver, Sender};
use std::{io, net::SocketAddr};
use std::net::SocketAddr;
use thiserror::Error;
use tokio::{net::UdpSocket, task::JoinHandle};
use anyhow::Result;
use tokio::{
net::UdpSocket,
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{event::Event, frontend::FrontendEvent};
use super::Server;
use lan_mouse_proto::{ProtoEvent, ProtocolError};
pub(crate) async fn new(
pub async fn new(
server: Server,
udp_recv_tx: Sender<Result<(ProtoEvent, SocketAddr), NetworkError>>,
udp_send_rx: Receiver<(ProtoEvent, SocketAddr)>,
) -> io::Result<JoinHandle<()>> {
frontend_notify_tx: Sender<FrontendEvent>,
) -> Result<(
JoinHandle<()>,
Sender<(Event, SocketAddr)>,
Receiver<Result<(Event, SocketAddr)>>,
Sender<u16>,
)> {
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get());
let mut socket = UdpSocket::bind(listen_addr).await?;
let (receiver_tx, receiver_rx) = tokio::sync::mpsc::channel(32);
let (sender_tx, mut sender_rx) = tokio::sync::mpsc::channel(32);
let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32);
Ok(tokio::task::spawn_local(async move {
let mut sender_rx = udp_send_rx;
let udp_task = tokio::task::spawn_local(async move {
loop {
let udp_receiver = udp_receiver(&socket, &udp_recv_tx);
let udp_sender = udp_sender(&socket, &mut sender_rx);
tokio::select! {
_ = udp_receiver => break, /* channel closed */
_ = udp_sender => break, /* channel closed */
_ = server.notifies.port_changed.notified() => update_port(&server, &mut socket).await,
_ = server.cancelled() => break, /* cancellation requested */
event = receive_event(&socket) => {
let _ = receiver_tx.send(event).await;
}
event = sender_rx.recv() => {
let Some((event, addr)) = event else {
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;
}
}
}
}
}
}))
});
Ok((udp_task, sender_tx, receiver_rx, port_tx))
}
async fn update_port(server: &Server, socket: &mut UdpSocket) {
let new_port = server.port.get();
let current_port = socket.local_addr().expect("socket not bound").port();
// if port is the same, we dont need to change it
if current_port == new_port {
return;
}
// bind new socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), new_port);
let new_socket = UdpSocket::bind(listen_addr).await;
let err = match new_socket {
Ok(new_socket) => {
*socket = new_socket;
None
}
Err(e) => Some(e.to_string()),
};
// notify frontend of the actual port
let port = socket.local_addr().expect("socket not bound").port();
server.notify_port_changed(port, err);
async fn receive_event(socket: &UdpSocket) -> Result<(Event, SocketAddr)> {
let mut buf = vec![0u8; 22];
let (_amt, src) = socket.recv_from(&mut buf).await?;
Ok((Event::try_from(buf)?, src))
}
async fn udp_receiver(
socket: &UdpSocket,
receiver_tx: &Sender<Result<(ProtoEvent, SocketAddr), NetworkError>>,
) {
loop {
let event = receive_event(socket).await;
receiver_tx.send(event).expect("channel closed");
}
}
async fn udp_sender(socket: &UdpSocket, rx: &mut Receiver<(ProtoEvent, SocketAddr)>) {
loop {
let (event, addr) = rx.recv().await.expect("channel closed");
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<(ProtoEvent, SocketAddr), NetworkError> {
let mut buf = [0u8; lan_mouse_proto::MAX_EVENT_SIZE];
let (_len, src) = socket.recv_from(&mut buf).await?;
let event = ProtoEvent::try_from(buf)?;
Ok((event, src))
}
fn send_event(sock: &UdpSocket, e: ProtoEvent, addr: SocketAddr) -> Result<usize, NetworkError> {
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let (data, len): ([u8; lan_mouse_proto::MAX_EVENT_SIZE], usize) = e.into();
let data: Vec<u8> = (&e).into();
// When udp blocks, we dont want to block the event loop.
// Dropping events is better than potentially crashing the input capture.
Ok(sock.try_send_to(&data[..len], addr)?)
Ok(sock.try_send_to(&data, addr)?)
}

View File

@@ -1,138 +1,129 @@
use std::{net::SocketAddr, time::Duration};
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::Sender;
use tokio::task::JoinHandle;
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use lan_mouse_ipc::ClientHandle;
use crate::{client::ClientHandle, event::Event};
use super::{capture_task::CaptureRequest, emulation_task::EmulationRequest, Server, State};
use super::{capture_task::CaptureEvent, emulation_task::EmulationEvent, Server, State};
const MAX_RESPONSE_TIME: Duration = Duration::from_millis(500);
pub(crate) fn new(
pub fn new(
server: Server,
sender_ch: Sender<(ProtoEvent, SocketAddr)>,
emulate_notify: Sender<EmulationRequest>,
capture_notify: Sender<CaptureRequest>,
sender_ch: Sender<(Event, SocketAddr)>,
emulate_notify: Sender<EmulationEvent>,
capture_notify: Sender<CaptureEvent>,
mut timer_rx: Receiver<()>,
) -> JoinHandle<()> {
// timer task
tokio::task::spawn_local(async move {
tokio::select! {
_ = server.notifies.cancel.cancelled() => {}
_ = ping_task(&server, sender_ch, emulate_notify, capture_notify) => {}
}
})
}
async fn ping_task(
server: &Server,
sender_ch: Sender<(ProtoEvent, SocketAddr)>,
emulate_notify: Sender<EmulationRequest>,
capture_notify: Sender<CaptureRequest>,
) {
loop {
// wait for wake up signal
server.ping_timer_notified().await;
let ping_task = tokio::task::spawn_local(async move {
loop {
let receiving = server.state.get() == State::Receiving;
let (ping_clients, ping_addrs) = {
let mut client_manager = server.client_manager.borrow_mut();
// wait for wake up signal
let Some(_): Option<()> = timer_rx.recv().await else {
break;
};
loop {
let receiving = server.state.get() == State::Receiving;
let (ping_clients, ping_addrs) = {
let mut client_manager = server.client_manager.borrow_mut();
let ping_clients: Vec<ClientHandle> = if receiving {
// if receiving we care about clients with pressed keys
client_manager
.get_client_states()
.filter(|(_, (_, s))| s.has_pressed_keys)
.map(|(h, _)| h)
.collect()
} else {
// if sending we care about the active client
server.active_client.get().iter().cloned().collect()
let ping_clients: Vec<ClientHandle> = if receiving {
// if receiving we care about clients with pressed keys
client_manager
.get_client_states_mut()
.filter(|(_, (_, s))| !s.pressed_keys.is_empty())
.map(|(h, _)| h)
.collect()
} else {
// if sending we care about the active client
server.active_client.get().iter().cloned().collect()
};
// get relevant socket addrs for clients
let ping_addrs: Vec<SocketAddr> = {
ping_clients
.iter()
.flat_map(|&h| client_manager.get(h))
.flat_map(|(c, s)| {
if s.alive && s.active_addr.is_some() {
vec![s.active_addr.unwrap()]
} else {
s.ips
.iter()
.cloned()
.map(|ip| SocketAddr::new(ip, c.port))
.collect()
}
})
.collect()
};
// reset alive
for (_, (_, s)) in client_manager.get_client_states_mut() {
s.alive = false;
}
(ping_clients, ping_addrs)
};
// get relevant socket addrs for clients
let ping_addrs: Vec<SocketAddr> = {
if receiving && ping_clients.is_empty() {
// receiving and no client has pressed keys
// -> no need to keep pinging
break;
}
// ping clients
for addr in ping_addrs {
if sender_ch.send((Event::Ping(), addr)).await.is_err() {
break;
}
}
// give clients time to resond
if receiving {
log::trace!("waiting {MAX_RESPONSE_TIME:?} for response from client with pressed keys ...");
} else {
log::trace!(
"state: {:?} => waiting {MAX_RESPONSE_TIME:?} for client to respond ...",
server.state.get()
);
}
tokio::time::sleep(MAX_RESPONSE_TIME).await;
// when anything is received from a client,
// the alive flag gets set
let unresponsive_clients: Vec<_> = {
let client_manager = server.client_manager.borrow();
ping_clients
.iter()
.flat_map(|&h| client_manager.get(h))
.flat_map(|(c, s)| {
if s.alive && s.active_addr.is_some() {
vec![s.active_addr.unwrap()]
} else {
s.ips
.iter()
.cloned()
.map(|ip| SocketAddr::new(ip, c.port))
.collect()
}
.filter_map(|&h| match client_manager.get(h) {
Some((_, s)) if !s.alive => Some(h),
_ => None,
})
.collect()
};
// reset alive
for (_, (_, s)) in client_manager.get_client_states_mut() {
s.alive = false;
}
(ping_clients, ping_addrs)
};
if receiving && ping_clients.is_empty() {
// receiving and no client has pressed keys
// -> no need to keep pinging
break;
}
// ping clients
for addr in ping_addrs {
if sender_ch.send((ProtoEvent::Ping, addr)).is_err() {
break;
}
}
// give clients time to resond
if receiving {
log::trace!(
"waiting {MAX_RESPONSE_TIME:?} for response from client with pressed keys ..."
);
} else {
log::trace!(
"state: {:?} => waiting {MAX_RESPONSE_TIME:?} for client to respond ...",
server.state.get()
);
}
tokio::time::sleep(MAX_RESPONSE_TIME).await;
// when anything is received from a client,
// the alive flag gets set
let unresponsive_clients: Vec<_> = {
let client_manager = server.client_manager.borrow();
ping_clients
.iter()
.filter_map(|&h| match client_manager.get(h) {
Some((_, s)) if !s.alive => Some(h),
_ => None,
})
.collect()
};
// we may not be receiving anymore but we should respond
// to the original state and not the "new" one
if receiving {
for h in unresponsive_clients {
log::warn!("device not responding, releasing keys!");
let _ = emulate_notify.send(EmulationRequest::ReleaseKeys(h));
}
} else {
// release pointer if the active client has not responded
if !unresponsive_clients.is_empty() {
log::warn!("client not responding, releasing pointer!");
server.state.replace(State::Receiving);
let _ = capture_notify.send(CaptureRequest::Release);
// we may not be receiving anymore but we should respond
// to the original state and not the "new" one
if receiving {
for h in unresponsive_clients {
log::warn!("device not responding, releasing keys!");
let _ = emulate_notify.send(EmulationEvent::ReleaseKeys(h)).await;
}
} else {
// release pointer if the active client has not responded
if !unresponsive_clients.is_empty() {
log::warn!("client not responding, releasing pointer!");
server.state.replace(State::Receiving);
let _ = capture_notify.send(CaptureEvent::Release).await;
}
}
}
}
}
});
ping_task
}

View File

@@ -0,0 +1,68 @@
use std::collections::HashSet;
use tokio::{sync::mpsc::Sender, task::JoinHandle};
use crate::{client::ClientHandle, dns::DnsResolver, frontend::FrontendEvent};
use super::Server;
#[derive(Clone)]
pub struct DnsRequest {
pub hostname: String,
pub handle: ClientHandle,
}
pub fn new(
resolver: DnsResolver,
mut server: Server,
mut frontend: Sender<FrontendEvent>,
) -> (JoinHandle<()>, Sender<DnsRequest>) {
let (dns_tx, mut dns_rx) = tokio::sync::mpsc::channel::<DnsRequest>(32);
let resolver_task = tokio::task::spawn_local(async move {
loop {
let (host, handle) = match dns_rx.recv().await {
Some(r) => (r.hostname, r.handle),
None => break,
};
/* update resolving status */
if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) {
s.resolving = true;
}
notify_state_change(&mut frontend, &mut server, handle).await;
let ips = match resolver.resolve(&host).await {
Ok(ips) => ips,
Err(e) => {
log::warn!("could not resolve host '{host}': {e}");
vec![]
}
};
/* update ips and resolving state */
if let Some((c, s)) = server.client_manager.borrow_mut().get_mut(handle) {
let mut addrs = HashSet::from_iter(c.fix_ips.iter().cloned());
for ip in ips {
addrs.insert(ip);
}
s.ips = addrs;
s.resolving = false;
}
notify_state_change(&mut frontend, &mut server, handle).await;
}
});
(resolver_task, dns_tx)
}
async fn notify_state_change(
frontend: &mut Sender<FrontendEvent>,
server: &mut Server,
handle: ClientHandle,
) {
let state = server.client_manager.borrow_mut().get_mut(handle).cloned();
if let Some((config, state)) = state {
let _ = frontend
.send(FrontendEvent::State(handle, config, state))
.await;
}
}