Compare commits

..

31 Commits

Author SHA1 Message Date
Ferdinand Schober
4703a4c947 chore: Release 2024-11-07 12:49:59 +01:00
Ferdinand Schober
a870a9e3a9 split features for emulation and capture backends 2024-11-07 12:43:42 +01:00
Ferdinand Schober
1433a3021b fix conditional compilation for xdp only build 2024-11-07 12:21:38 +01:00
Ferdinand Schober
ef92bd69d9 Update README.md 2024-11-07 02:37:40 +01:00
Ferdinand Schober
526d8004b6 Update README.md 2024-11-07 02:36:11 +01:00
Ferdinand Schober
fef702ffdd Update README.md (#229) 2024-11-07 02:35:32 +01:00
Ferdinand Schober
41ab25cc19 update screenshots (#228) 2024-11-07 01:18:55 +01:00
Ferdinand Schober
66456f18f1 update core-graphics / foundation (#227) 2024-11-07 01:15:35 +01:00
Ferdinand Schober
1d25dfbe50 upgrade ashpd + reis (#226) 2024-11-07 00:38:26 +01:00
Ferdinand Schober
9d28fe6c7b bump dependencies 2024-11-06 23:53:42 +01:00
byquanton
de674c631a Rename Synergy Community Edition to Deskflow (#225) 2024-11-06 13:46:19 +01:00
Ferdinand Schober
cd2cabf25b adapt config backend options to cli arg names 2024-11-06 12:14:47 +01:00
Ferdinand Schober
71f7e2e5e0 fix left over capture barrier 2024-11-05 14:32:39 +01:00
Ferdinand Schober
e7a18b9696 recreate wl_{pointer,keyboard} on capability event (#222)
this should fix #88
fixes #123
2024-11-04 22:45:57 +01:00
Ferdinand Schober
7496015d8d macos: implement client side modifier events (#219)
closes #198
closes #199
2024-10-27 08:41:26 +01:00
Ferdinand Schober
75b790ec2e propagate event tap creation error (#218) 2024-10-25 16:31:15 +02:00
Emile Akbarzadeh
9f52a2ac93 Add default.nix file to main and update readme (#211)
Co-authored-by: Emile Akbarzadeh <emile@dromeda.com.au>
2024-10-25 16:20:15 +02:00
Ferdinand Schober
4856199153 fix windows ci 2024-10-25 15:12:20 +02:00
虢豳
24e7a1d07f Add desktop and icon (#212)
* nix flake update

* nix: add desktop and icon

closes #210
2024-10-07 12:05:07 +02:00
Ferdinand Schober
555fbfeb79 formatting 2024-10-05 21:48:49 +02:00
Ferdinand Schober
6191216873 windows: fix panic when recreating input-capture
RegisterWindowClassW fails when called again
2024-10-05 21:47:12 +02:00
Ferdinand Schober
5b1dc4ccf8 reference count capture (#209)
* reference count capture

Multiple captures can now be created at the same position.
Captures at the same position are reference counted.

* update testcase

will be required by #200 / #164
2024-10-05 21:22:28 +02:00
Ferdinand Schober
ab1b45ff45 macos: fix key-release with repeat logic (#206) 2024-10-04 18:21:40 +02:00
Jacob Barber
b071201dcb Fix multimonitors (#202)
Co-authored-by: Jacob Barber <jacob.barber@disney.com>

closes #83
2024-09-20 20:50:37 +02:00
Nick Bolton
f52f19d2e3 Add link to Synergy (open source) (#194) 2024-09-10 19:06:09 +02:00
Ferdinand Schober
39fed0344c cleanup server code + fix a lost update case (#191) 2024-09-05 02:31:10 +02:00
Ferdinand Schober
6cd190191e cleanup main (#189) 2024-09-05 01:06:55 +02:00
Ferdinand Schober
be677d4c81 extract frontend crate (#186) 2024-09-04 17:29:29 +02:00
Ferdinand Schober
12bc0d86ca layer-shell: drop hard-dep on shortcut-inhibit (#188)
soften dependencies off layer-shell backend to make
https://wayland.app/protocols/keyboard-shortcuts-inhibit-unstable-v1
optional.

This allows partial functionality on compositors that don't support
the protocol, e.g. labwc.
2024-09-03 23:24:14 +02:00
Ferdinand Schober
1f7a7309eb include commit-hash in version (#185) 2024-09-02 19:46:07 +02:00
Ferdinand Schober
8926d8f803 produce events in dummy capture-backend (#184) 2024-09-02 17:59:17 +02:00
62 changed files with 2660 additions and 2032 deletions

View File

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

View File

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

View File

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

1098
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,18 @@
[workspace] [workspace]
members = ["input-capture", "input-emulation", "input-event", "lan-mouse-proto"] members = [
"input-capture",
"input-emulation",
"input-event",
"lan-mouse-ipc",
"lan-mouse-cli",
"lan-mouse-gtk",
"lan-mouse-proto",
]
[package] [package]
name = "lan-mouse" name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks" description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.9.1" version = "0.10.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -14,15 +22,17 @@ strip = true
lto = "fat" lto = "fat"
[dependencies] [dependencies]
input-event = { path = "input-event", version = "0.2.1" } input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.2.1", default-features = false } input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
input-capture = { path = "input-capture", version = "0.2.0", default-features = false } input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.1.0" } 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" }
hickory-resolver = "0.24.1" hickory-resolver = "0.24.1"
toml = "0.8" toml = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71"
log = "0.4.20" log = "0.4.20"
env_logger = "0.11.3" env_logger = "0.11.3"
serde_json = "1.0.107" serde_json = "1.0.107"
@@ -38,30 +48,30 @@ tokio = { version = "1.32.0", features = [
] } ] }
futures = "0.3.28" futures = "0.3.28"
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.4.11", features = ["derive"] }
gtk = { package = "gtk4", version = "0.9.0", features = [
"v4_2",
], optional = true }
adw = { package = "libadwaita", version = "0.7.0", features = [
"v1_1",
], optional = true }
async-channel = { version = "2.1.1", optional = true }
hostname = "0.4.0"
slab = "0.4.9" slab = "0.4.9"
endi = "1.1.0" thiserror = "2.0.0"
thiserror = "1.0.61"
tokio-util = "0.7.11" tokio-util = "0.7.11"
local-channel = "0.1.5" local-channel = "0.1.5"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.148" libc = "0.2.148"
[build-dependencies]
glib-build-tools = { version = "0.20.0", optional = true }
[features] [features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"] default = [
wayland = ["input-capture/wayland", "input-emulation/wayland"] "gtk",
x11 = ["input-capture/x11", "input-emulation/x11"] "layer_shell_capture",
xdg_desktop_portal = ["input-emulation/xdg_desktop_portal"] "x11_capture",
libei = ["input-event/libei", "input-capture/libei", "input-emulation/libei"] "libei_capture",
gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"] "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"]

326
README.md
View File

@@ -1,78 +1,65 @@
# Lan Mouse # Lan Mouse
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. Lan Mouse is a *cross-platform* 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. It allows for using multiple PCs via a single set of mouse and keyboard.
This is also known as a Software KVM switch. 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). 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.
- _Now with a gtk frontend_ - _Now with a gtk frontend_
<picture> <picture>
<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: dark)" srcset="/screenshots/dark.png?raw=true">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709"> <source media="(prefers-color-scheme: light)" srcset="/screenshots/light.png?raw=true">
<img alt="Screenshot of Lan-Mouse" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df"> <img alt="Screenshot of Lan-Mouse" srcset="/screenshots/dark.png">
</picture> </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] > [!WARNING]
> Since this tool has gained a bit of popularity over the past couple of days: > DISCLAIMER:
> > Until [#200](https://github.com/feschber/lan-mouse/pull/200) is merged, all network traffic is **unencrypted** and sent in **plaintext**!
> 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. > 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 for now > Therefore you should only use this tool in your local network with trusted devices.
> and I take no responsibility for any leakage of data! > I take no responsibility for any security breaches!
## OS Support ## OS Support
The following table shows support for input emulation (to emulate events received from other clients) and Most current desktop environments and operating systems are fully supported, this includes
input capture (to send events *to* other clients) on different operating systems: - GNOME >= 45
- KDE Plasma >= 6.1
- Most wlroots based compositors, including Sway (>= 1.8), Hyprland and Wayfire
- Windows
- MacOS
| OS / Desktop Environment | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------| ### Caveats / Known Issues
| 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] > [!Important]
> Gnome -> Sway only partially works (modifier events are not handled correctly) > - **X11** currently only has support for input emulation, i.e. can only be used on the receiving end.
> [!Important]
> **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!** > - **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!**
> Otherwise input capture will not work. > Otherwise input capture will not work.
>
> [!Important] > - **Windows**: The mouse cursor will be invisible when sending input to a Windows system if
> The mouse cursor will be invisible when sending input to a Windows system if
> there is no real mouse connected to the machine. > there is no real mouse connected to the machine.
For more detailed information about os support see [Detailed OS Support](#detailed-os-support)
## Installation ## Installation
### Install via cargo
```sh
cargo install lan-mouse
```
### Download from Releases <details>
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases). <summary>Arch Linux</summary>
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/): Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
@@ -80,43 +67,41 @@ Lan Mouse can be installed from the [official repositories](https://archlinux.or
pacman -S lan-mouse pacman -S lan-mouse
``` ```
It is also available on the AUR: The prerelease version (following `main`) is available on the AUR:
```sh ```sh
# git version (includes latest changes)
paru -S lan-mouse-git 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) - 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) - flake: [README.md](./nix/README.md)
</details>
### Manual Installation
<details>
<summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies). First make sure to [install the necessary dependencies](#installing-dependencies).
Build in release mode: Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
```sh For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
cargo build --release
```
Run directly: Alternatively, the `lan-mouse` binary can be compiled from source (see below).
```sh
cargo run --release
```
Install the files: ### Installing desktop file, app icon and firewall rules (optional)
```sh ```sh
# install lan-mouse # install lan-mouse (replace path/to/ with the correct path)
sudo cp target/release/lan-mouse /usr/local/bin/ sudo cp path/to/lan-mouse /usr/local/bin/
# install app icon # install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
sudo cp resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps sudo cp lan-mouse-gtk/resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
# update icon cache # update icon cache
gtk-update-icon-cache /usr/local/share/icons/hicolor/ gtk-update-icon-cache /usr/local/share/icons/hicolor/
@@ -130,17 +115,34 @@ sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings # -> enable the service in firewalld settings
``` ```
### Conditional Compilation Instead of downloading from the releases, the `lan-mouse` binary
can be easily compiled via cargo or nix:
Currently only x11, wayland, windows and MacOS are supported backends. ### Compiling and installing manually:
Depending on the toolchain used, support for other platforms is omitted ```sh
automatically (it does not make sense to build a Windows `.exe` with # compile in release mode
support for x11 and wayland backends). cargo build --release
However one might still want to omit support for e.g. wayland, x11 or libei on # install lan-mouse
a Linux system. sudo cp target/release/lan-mouse /usr/local/bin/
```
This is possible through ### 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
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html). [cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces E.g. if only wayland support is needed, the following command produces
@@ -149,14 +151,17 @@ an executable with just support for wayland:
cargo build --no-default-features --features wayland cargo build --no-default-features --features wayland
``` ```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml) For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
</details>
## Installing Dependencies
## Installing Dependencies for Development / Compiling from Source
<details> <details>
<summary>MacOS</summary> <summary>MacOS</summary>
```sh ```sh
brew install libadwaita brew install libadwaita pkg-config
``` ```
</details> </details>
@@ -184,10 +189,22 @@ sudo dnf install libadwaita-devel libXtst-devel libX11-devel
``` ```
</details> </details>
<details> <details>
<summary>Windows</summary> <summary>Nix</summary>
```sh
nix-shell .
```
</details>
<details>
<summary>Nix (flake)</summary>
```sh
nix develop
```
</details>
> [!NOTE] <details>
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls. <summary>Windows</summary>
- First install [Rust](https://www.rust-lang.org/tools/install). - First install [Rust](https://www.rust-lang.org/tools/install).
@@ -219,18 +236,20 @@ python -m pipx ensurepath
pipx install gvsbuild pipx install gvsbuild
# build gtk + libadwaita # build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg gvsbuild build gtk4 libadwaita librsvg adwaita-icon-theme
``` ```
- **Make sure to add the directory** `C:\gtk-build\gtk\x64\release\bin` - **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 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 To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below). the gtk frontend (see conditional compilation).
</details> </details>
## Usage ## Usage
### Gtk Frontend <details>
<summary>Gtk Frontend</summary>
By default the gtk frontend will open when running `lan-mouse`. By default the gtk frontend will open when running `lan-mouse`.
To add a new connection, simply click the `Add` button on *both* devices, To add a new connection, simply click the `Add` button on *both* devices,
@@ -238,8 +257,11 @@ 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) 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. 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. The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands. Type `help` to list the available commands.
@@ -253,13 +275,17 @@ $ cargo run --release -- --frontend cli
(...) (...)
> activate 0 > 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: To do so, add `--daemon` to the commandline args:
```sh ```sh
$ cargo run --release -- --daemon lan-mouse --daemon
``` ```
In order to start lan-mouse with a graphical session automatically, In order to start lan-mouse with a graphical session automatically,
@@ -272,6 +298,7 @@ cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service systemctl --user enable --now lan-mouse.service
``` ```
</details>
## Configuration ## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed. To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
@@ -327,109 +354,60 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
- [x] Liveness tracking: Automatically release keys, when server offline - [x] Liveness tracking: Automatically release keys, when server offline
- [x] MacOS KeyCode Translation - [x] MacOS KeyCode Translation
- [x] Libei Input Capture - [x] Libei Input Capture
- [x] MacOS Input Capture
- [x] Windows Input Capture
- [ ] *Encryption* (WIP)
- [ ] X11 Input Capture - [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] Latency measurement and visualization - [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization - [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support - [ ] 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.
## Wayland support ## Detailed OS Support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
For this reason a suitable backend is chosen based on the active desktop environment / compositor. 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.
Different compositors have different ways of enabling input emulation: A suitable backend is chosen automatically based on the active desktop environment / compositor.
#### Wlroots The following sections detail the emulation and capture backends provided by lan-mouse and their support in desktop environments / operating systems.
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)
#### KDE ### Input Emulation Support
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.
The recommended way to emulate input on KDE is the | Desktop / Backend | layer-shell | libei | windows | macos | x11 |
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop). |---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| 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 |
#### Gnome - `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.
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation and capture, - `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
which has the goal to become the general approach for emulating and capturing Input on Wayland. - `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.
### 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
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome | ### Input Capture Support
|----------------------------------------|--------------------|----------------------|----------------------|
| 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: |
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required | Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
to display surfaces on screen edges and used to display the immovable window on |---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
both wlroots based compositors and KDE. | 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)
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

@@ -12,12 +12,4 @@ fn main() {
let git_describe = String::from_utf8(git_describe.stdout).unwrap(); let git_describe = String::from_utf8(git_describe.stdout).unwrap();
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}"); 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",
);
} }

3
default.nix Normal file
View File

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

46
flake.lock generated
View File

@@ -1,30 +1,12 @@
{ {
"nodes": { "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1716293225, "lastModified": 1728018373,
"narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=", "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916", "rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -42,17 +24,16 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1716257780, "lastModified": 1728181869,
"narHash": "sha256-R+NjvJzKEkTVCmdrKRfPE4liX/KMGVqGUwwS5H8ET8A=", "narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "4e5e3d2c5c9b2721bd266f9e43c14e96811b89d2", "rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -60,21 +41,6 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "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", "root": "root",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "input-capture" name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse" description = "cross-platform input-capture library used by lan-mouse"
version = "0.2.0" version = "0.3.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -10,10 +10,10 @@ repository = "https://github.com/feschber/lan-mouse"
futures = "0.3.28" futures = "0.3.28"
futures-core = "0.3.30" futures-core = "0.3.30"
log = "0.4.22" log = "0.4.22"
input-event = { path = "../input-event", version = "0.2.1" } input-event = { path = "../input-event", version = "0.3.0" }
memmap = "0.7" memmap = "0.7"
tempfile = "3.8" tempfile = "3.8"
thiserror = "1.0.61" thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [ tokio = { version = "1.32.0", features = [
"io-util", "io-util",
"io-std", "io-std",
@@ -40,18 +40,18 @@ wayland-protocols-wlr = { version = "0.3.1", features = [
"client", "client",
], optional = true } ], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.9", default-features = false, features = [ ashpd = { version = "0.10", default-features = false, features = [
"tokio", "tokio",
], optional = true } ], optional = true }
reis = { version = "0.2", features = ["tokio"], optional = true } reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies] [target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] } core-graphics = { version = "0.24.0", features = ["highsierra"] }
core-foundation = "0.9.4" core-foundation = "0.10.0"
core-foundation-sys = "0.8.6" core-foundation-sys = "0.8.6"
libc = "0.2.155" libc = "0.2.155"
keycode = "0.4.0" keycode = "0.4.0"
bitflags = "2.5.0" bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [ windows = { version = "0.58.0", features = [
@@ -65,8 +65,8 @@ windows = { version = "0.58.0", features = [
] } ] }
[features] [features]
default = ["wayland", "x11", "libei"] default = ["layer_shell", "x11", "libei"]
wayland = [ layer_shell = [
"dep:wayland-client", "dep:wayland-client",
"dep:wayland-protocols", "dep:wayland-protocols",
"dep:wayland-protocols-wlr", "dep:wayland-protocols-wlr",

View File

@@ -1,16 +1,28 @@
use std::f64::consts::PI;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{ready, Context, Poll};
use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use futures_core::Stream; use futures_core::Stream;
use input_event::PointerEvent;
use tokio::time::{self, Instant, Interval};
use super::{Capture, CaptureError, CaptureEvent, CaptureHandle, Position}; use super::{Capture, CaptureError, CaptureEvent, Position};
pub struct DummyInputCapture {} pub struct DummyInputCapture {
start: Option<Instant>,
interval: Interval,
offset: (i32, i32),
}
impl DummyInputCapture { impl DummyInputCapture {
pub fn new() -> Self { pub fn new() -> Self {
Self {} Self {
start: None,
interval: time::interval(Duration::from_millis(1)),
offset: (0, 0),
}
} }
} }
@@ -22,11 +34,11 @@ impl Default for DummyInputCapture {
#[async_trait] #[async_trait]
impl Capture for DummyInputCapture { impl Capture for DummyInputCapture {
async fn create(&mut self, _handle: CaptureHandle, _pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(()) Ok(())
} }
async fn destroy(&mut self, _handle: CaptureHandle) -> Result<(), CaptureError> { async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(()) Ok(())
} }
@@ -39,10 +51,36 @@ impl Capture for DummyInputCapture {
} }
} }
impl Stream for DummyInputCapture { const FREQUENCY_HZ: f64 = 1.0;
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; const RADIUS: f64 = 100.0;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { impl Stream for DummyInputCapture {
Poll::Pending 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

@@ -8,9 +8,9 @@ pub enum InputCaptureError {
Capture(#[from] CaptureError), Capture(#[from] CaptureError),
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use std::io; use std::io;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
@@ -19,26 +19,10 @@ use wayland_client::{
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use ashpd::desktop::ResponseError; use ashpd::desktop::ResponseError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::{EiConvertEventStreamError, HandshakeError};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use core_graphics::base::CGError; use core_graphics::base::CGError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("error in libei stream: {inner:?}")]
pub struct ReisConvertEventStreamError {
inner: EiConvertEventStreamError,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl From<EiConvertEventStreamError> for ReisConvertEventStreamError {
fn from(e: EiConvertEventStreamError) -> Self {
Self { inner: e }
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum CaptureError { pub enum CaptureError {
#[error("activation stream closed unexpectedly")] #[error("activation stream closed unexpectedly")]
@@ -48,11 +32,8 @@ pub enum CaptureError {
#[error("io error: `{0}`")] #[error("io error: `{0}`")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error in libei stream: `{0}`")] #[error("libei error: `{0}`")]
Reis(#[from] ReisConvertEventStreamError), Reis(#[from] reis::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei handshake failed: `{0}`")]
Handshake(#[from] HandshakeError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error(transparent)] #[error(transparent)]
Portal(#[from] ashpd::Error), Portal(#[from] ashpd::Error),
@@ -83,7 +64,7 @@ pub enum CaptureCreationError {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error creating input-capture-portal backend: `{0}`")] #[error("error creating input-capture-portal backend: `{0}`")]
Libei(#[from] LibeiCaptureCreationError), Libei(#[from] LibeiCaptureCreationError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[error("error creating layer-shell capture backend: `{0}`")] #[error("error creating layer-shell capture backend: `{0}`")]
LayerShell(#[from] LayerShellCaptureCreationError), LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
@@ -93,7 +74,7 @@ pub enum CaptureCreationError {
#[error("error creating windows capture backend")] #[error("error creating windows capture backend")]
Windows, Windows,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[error("error creating macos capture backend")] #[error("error creating macos capture backend: `{0}`")]
MacOS(#[from] MacosCaptureCreationError), MacOS(#[from] MacosCaptureCreationError),
} }
@@ -121,7 +102,7 @@ pub enum LibeiCaptureCreationError {
Ashpd(#[from] ashpd::Error), Ashpd(#[from] ashpd::Error),
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("{protocol} protocol not supported: {inner}")] #[error("{protocol} protocol not supported: {inner}")]
pub struct WaylandBindError { pub struct WaylandBindError {
@@ -129,14 +110,14 @@ pub struct WaylandBindError {
protocol: &'static str, protocol: &'static str,
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
impl WaylandBindError { impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self { pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol } Self { inner, protocol }
} }
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError { pub enum LayerShellCaptureCreationError {
#[error(transparent)] #[error(transparent)]
@@ -165,6 +146,9 @@ pub enum X11InputCaptureCreationError {
pub enum MacosCaptureCreationError { pub enum MacosCaptureCreationError {
#[error("event source creation failed!")] #[error("event source creation failed!")]
EventSourceCreation, EventSourceCreation,
#[cfg(target_os = "macos")]
#[error("event tap creation failed")]
EventTapCreation,
#[error("failed to set CG Cursor property")] #[error("failed to set CG Cursor property")]
CGCursorProperty, CGCursorProperty,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View File

@@ -64,14 +64,14 @@ use crate::{CaptureError, CaptureEvent};
use super::{ use super::{
error::{LayerShellCaptureCreationError, WaylandBindError}, error::{LayerShellCaptureCreationError, WaylandBindError},
Capture, CaptureHandle, Position, Capture, Position,
}; };
struct Globals { struct Globals {
compositor: wl_compositor::WlCompositor, compositor: wl_compositor::WlCompositor,
pointer_constraints: ZwpPointerConstraintsV1, pointer_constraints: ZwpPointerConstraintsV1,
relative_pointer_manager: ZwpRelativePointerManagerV1, relative_pointer_manager: ZwpRelativePointerManagerV1,
shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1, shortcut_inhibit_manager: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
seat: wl_seat::WlSeat, seat: wl_seat::WlSeat,
shm: wl_shm::WlShm, shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1, layer_shell: ZwlrLayerShellV1,
@@ -102,13 +102,13 @@ struct State {
pointer_lock: Option<ZwpLockedPointerV1>, pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>, rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>, shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
client_for_window: Vec<(Arc<Window>, CaptureHandle)>, active_windows: Vec<Arc<Window>>,
focused: Option<(Arc<Window>, CaptureHandle)>, focused: Option<Arc<Window>>,
g: Globals, g: Globals,
wayland_fd: RawFd, wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>, read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>, qh: QueueHandle<Self>,
pending_events: VecDeque<(CaptureHandle, CaptureEvent)>, pending_events: VecDeque<(Position, CaptureEvent)>,
output_info: Vec<(WlOutput, OutputInfo)>, output_info: Vec<(WlOutput, OutputInfo)>,
scroll_discrete_pending: bool, scroll_discrete_pending: bool,
} }
@@ -124,7 +124,7 @@ impl AsRawFd for Inner {
} }
} }
pub struct WaylandInputCapture(AsyncFd<Inner>); pub struct LayerShellInputCapture(AsyncFd<Inner>);
struct Window { struct Window {
buffer: wl_buffer::WlBuffer, buffer: wl_buffer::WlBuffer,
@@ -256,7 +256,7 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
} }
} }
impl WaylandInputCapture { impl LayerShellInputCapture {
pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> { pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
let conn = Connection::connect_to_env()?; let conn = Connection::connect_to_env()?;
let (g, mut queue) = registry_queue_init::<State>(&conn)?; let (g, mut queue) = registry_queue_init::<State>(&conn)?;
@@ -285,9 +285,18 @@ impl WaylandInputCapture {
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
.bind(&qh, 1..=1, ()) .bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?; .map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?;
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = g let shortcut_inhibit_manager: Result<
ZwpKeyboardShortcutsInhibitManagerV1,
WaylandBindError,
> = g
.bind(&qh, 1..=1, ()) .bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"))?; .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 outputs = vec![];
let g = Globals { let g = Globals {
@@ -314,7 +323,7 @@ impl WaylandInputCapture {
pointer_lock: None, pointer_lock: None,
rel_pointer: None, rel_pointer: None,
shortcut_inhibitor: None, shortcut_inhibitor: None,
client_for_window: Vec::new(), active_windows: Vec::new(),
focused: None, focused: None,
qh, qh,
wayland_fd, wayland_fd,
@@ -361,23 +370,18 @@ impl WaylandInputCapture {
let inner = AsyncFd::new(Inner { queue, state })?; let inner = AsyncFd::new(Inner { queue, state })?;
Ok(WaylandInputCapture(inner)) Ok(LayerShellInputCapture(inner))
} }
fn add_client(&mut self, handle: CaptureHandle, pos: Position) { fn add_client(&mut self, pos: Position) {
self.0.get_mut().state.add_client(handle, pos); self.0.get_mut().state.add_client(pos);
} }
fn delete_client(&mut self, handle: CaptureHandle) { fn delete_client(&mut self, pos: Position) {
let inner = self.0.get_mut(); let inner = self.0.get_mut();
// remove all windows corresponding to this client // remove all windows corresponding to this client
while let Some(i) = inner while let Some(i) = inner.state.active_windows.iter().position(|w| w.pos == pos) {
.state inner.state.active_windows.remove(i);
.client_for_window
.iter()
.position(|(_, c)| *c == handle)
{
inner.state.client_for_window.remove(i);
inner.state.focused = None; inner.state.focused = None;
} }
} }
@@ -391,7 +395,7 @@ impl State {
serial: u32, serial: u32,
qh: &QueueHandle<State>, qh: &QueueHandle<State>,
) { ) {
let (window, _) = self.focused.as_ref().unwrap(); let window = self.focused.as_ref().unwrap();
// hide the cursor // hide the cursor
pointer.set_cursor(serial, None, 0, 0); pointer.set_cursor(serial, None, 0, 0);
@@ -424,19 +428,17 @@ impl State {
} }
// capture modifier keys // capture modifier keys
if self.shortcut_inhibitor.is_none() { if let Some(shortcut_inhibit_manager) = &self.g.shortcut_inhibit_manager {
self.shortcut_inhibitor = Some(self.g.shortcut_inhibit_manager.inhibit_shortcuts( if self.shortcut_inhibitor.is_none() {
surface, self.shortcut_inhibitor =
&self.g.seat, Some(shortcut_inhibit_manager.inhibit_shortcuts(surface, &self.g.seat, qh, ()));
qh, }
(),
));
} }
} }
fn ungrab(&mut self) { fn ungrab(&mut self) {
// get focused client // get focused client
let (window, _client) = match self.focused.as_ref() { let window = match self.focused.as_ref() {
Some(focused) => focused, Some(focused) => focused,
None => return, None => return,
}; };
@@ -466,27 +468,23 @@ impl State {
} }
} }
fn add_client(&mut self, client: CaptureHandle, pos: Position) { fn add_client(&mut self, pos: Position) {
let outputs = get_output_configuration(self, pos); let outputs = get_output_configuration(self, pos);
log::debug!("outputs: {outputs:?}"); log::debug!("outputs: {outputs:?}");
outputs.iter().for_each(|(o, i)| { outputs.iter().for_each(|(o, i)| {
let window = Window::new(self, &self.qh, o, pos, i.size); let window = Window::new(self, &self.qh, o, pos, i.size);
let window = Arc::new(window); let window = Arc::new(window);
self.client_for_window.push((window, client)); self.active_windows.push(window);
}); });
} }
fn update_windows(&mut self) { fn update_windows(&mut self) {
log::debug!("updating windows"); log::debug!("updating windows");
log::debug!("output info: {:?}", self.output_info); log::debug!("output info: {:?}", self.output_info);
let clients: Vec<_> = self let clients: Vec<_> = self.active_windows.drain(..).map(|w| w.pos).collect();
.client_for_window for pos in clients {
.drain(..) self.add_client(pos);
.map(|(w, c)| (c, w.pos))
.collect();
for (client, pos) in clients {
self.add_client(client, pos);
} }
} }
} }
@@ -559,15 +557,15 @@ impl Inner {
} }
#[async_trait] #[async_trait]
impl Capture for WaylandInputCapture { impl Capture for LayerShellInputCapture {
async fn create(&mut self, handle: CaptureHandle, pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
self.add_client(handle, pos); self.add_client(pos);
let inner = self.0.get_mut(); let inner = self.0.get_mut();
Ok(inner.flush_events()?) Ok(inner.flush_events()?)
} }
async fn destroy(&mut self, handle: CaptureHandle) -> Result<(), CaptureError> { async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
self.delete_client(handle); self.delete_client(pos);
let inner = self.0.get_mut(); let inner = self.0.get_mut();
Ok(inner.flush_events()?) Ok(inner.flush_events()?)
} }
@@ -584,8 +582,8 @@ impl Capture for WaylandInputCapture {
} }
} }
impl Stream for WaylandInputCapture { impl Stream for LayerShellInputCapture {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() { if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
@@ -650,10 +648,16 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
capabilities: WEnum::Value(capabilities), capabilities: WEnum::Value(capabilities),
} = event } = event
{ {
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() { if capabilities.contains(wl_seat::Capability::Pointer) {
if let Some(p) = state.pointer.take() {
p.release();
}
state.pointer.replace(seat.get_pointer(qh, ())); state.pointer.replace(seat.get_pointer(qh, ()));
} }
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() { if capabilities.contains(wl_seat::Capability::Keyboard) {
if let Some(k) = state.keyboard.take() {
k.release();
}
seat.get_keyboard(qh, ()); seat.get_keyboard(qh, ());
} }
} }
@@ -678,23 +682,20 @@ impl Dispatch<WlPointer, ()> for State {
} => { } => {
// get client corresponding to the focused surface // get client corresponding to the focused surface
{ {
if let Some((window, client)) = app if let Some(window) = app.active_windows.iter().find(|w| w.surface == surface) {
.client_for_window app.focused = Some(window.clone());
.iter()
.find(|(w, _c)| w.surface == surface)
{
app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial, qh); app.grab(&surface, pointer, serial, qh);
} else { } else {
return; return;
} }
} }
let (_, client) = app let pos = app
.client_for_window .active_windows
.iter() .iter()
.find(|(w, _c)| w.surface == surface) .find(|w| w.surface == surface)
.map(|w| w.pos)
.unwrap(); .unwrap();
app.pending_events.push_back((*client, CaptureEvent::Begin)); app.pending_events.push_back((pos, CaptureEvent::Begin));
} }
wl_pointer::Event::Leave { .. } => { wl_pointer::Event::Leave { .. } => {
/* There are rare cases, where when a window is opened in /* There are rare cases, where when a window is opened in
@@ -715,9 +716,9 @@ impl Dispatch<WlPointer, ()> for State {
button, button,
state, state,
} => { } => {
let (_, client) = app.focused.as_ref().unwrap(); let window = app.focused.as_ref().unwrap();
app.pending_events.push_back(( app.pending_events.push_back((
*client, window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Button { CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time, time,
button, button,
@@ -726,7 +727,7 @@ impl Dispatch<WlPointer, ()> for State {
)); ));
} }
wl_pointer::Event::Axis { time, axis, value } => { wl_pointer::Event::Axis { time, axis, value } => {
let (_, client) = app.focused.as_ref().unwrap(); let window = app.focused.as_ref().unwrap();
if app.scroll_discrete_pending { if app.scroll_discrete_pending {
// each axisvalue120 event is coupled with // each axisvalue120 event is coupled with
// a corresponding axis event, which needs to // a corresponding axis event, which needs to
@@ -734,7 +735,7 @@ impl Dispatch<WlPointer, ()> for State {
app.scroll_discrete_pending = false; app.scroll_discrete_pending = false;
} else { } else {
app.pending_events.push_back(( app.pending_events.push_back((
*client, window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Axis { CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time, time,
axis: u32::from(axis) as u8, axis: u32::from(axis) as u8,
@@ -744,10 +745,10 @@ impl Dispatch<WlPointer, ()> for State {
} }
} }
wl_pointer::Event::AxisValue120 { axis, value120 } => { wl_pointer::Event::AxisValue120 { axis, value120 } => {
let (_, client) = app.focused.as_ref().unwrap(); let window = app.focused.as_ref().unwrap();
app.scroll_discrete_pending = true; app.scroll_discrete_pending = true;
app.pending_events.push_back(( app.pending_events.push_back((
*client, window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::AxisDiscrete120 { CaptureEvent::Input(Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: u32::from(axis) as u8, axis: u32::from(axis) as u8,
value: value120, value: value120,
@@ -773,10 +774,7 @@ impl Dispatch<WlKeyboard, ()> for State {
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, _: &QueueHandle<Self>,
) { ) {
let (_window, client) = match &app.focused { let window = &app.focused;
Some(focused) => (Some(&focused.0), Some(&focused.1)),
None => (None, None),
};
match event { match event {
wl_keyboard::Event::Key { wl_keyboard::Event::Key {
serial: _, serial: _,
@@ -784,9 +782,9 @@ impl Dispatch<WlKeyboard, ()> for State {
key, key,
state, state,
} => { } => {
if let Some(client) = client { if let Some(window) = window {
app.pending_events.push_back(( app.pending_events.push_back((
*client, window.pos,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time, time,
key, key,
@@ -802,9 +800,9 @@ impl Dispatch<WlKeyboard, ()> for State {
mods_locked, mods_locked,
group, group,
} => { } => {
if let Some(client) = client { if let Some(window) = window {
app.pending_events.push_back(( app.pending_events.push_back((
*client, window.pos,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers { CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers {
depressed: mods_depressed, depressed: mods_depressed,
latched: mods_latched, latched: mods_latched,
@@ -836,10 +834,10 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
.. ..
} = event } = event
{ {
if let Some((_window, client)) = &app.focused { if let Some(window) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32; let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push_back(( app.pending_events.push_back((
*client, window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })), CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })),
)); ));
} }
@@ -857,10 +855,10 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
_: &QueueHandle<Self>, _: &QueueHandle<Self>,
) { ) {
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event { if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
if let Some((window, _client)) = app if let Some(window) = app
.client_for_window .active_windows
.iter() .iter()
.find(|(w, _c)| &w.layer_surface == layer_surface) .find(|w| &w.layer_surface == layer_surface)
{ {
// client corresponding to the layer_surface // client corresponding to the layer_surface
let surface = &window.surface; let surface = &window.surface;

View File

@@ -1,4 +1,9 @@
use std::{collections::HashSet, fmt::Display, task::Poll}; use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Display,
mem::swap,
task::{ready, Poll},
};
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
@@ -16,8 +21,8 @@ mod libei;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod macos; mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
mod wayland; mod layer_shell;
#[cfg(windows)] #[cfg(windows)]
mod windows; mod windows;
@@ -82,7 +87,7 @@ impl Display for Position {
pub enum Backend { pub enum Backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal, InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
LayerShell, LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11, X11,
@@ -98,7 +103,7 @@ impl Display for Backend {
match self { match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => write!(f, "input-capture-portal"), Backend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => write!(f, "layer-shell"), Backend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"), Backend::X11 => write!(f, "X11"),
@@ -112,19 +117,52 @@ impl Display for Backend {
} }
pub struct InputCapture { pub struct InputCapture {
/// capture backend
capture: Box<dyn Capture>, capture: Box<dyn Capture>,
/// keys pressed by active capture
pressed_keys: HashSet<scancode::Linux>, 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 { impl InputCapture {
/// create a new client with the given id /// create a new client with the given id
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> { pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
self.capture.create(id, pos).await 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 /// destroy the client with the given id, if it exists
pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> { pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> {
self.capture.destroy(id).await 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 /// release mouse
@@ -143,6 +181,9 @@ impl InputCapture {
let capture = create(backend).await?; let capture = create(backend).await?;
Ok(Self { Ok(Self {
capture, capture,
id_map: Default::default(),
pending: Default::default(),
position_map: Default::default(),
pressed_keys: HashSet::new(), pressed_keys: HashSet::new(),
}) })
} }
@@ -170,29 +211,65 @@ impl Stream for InputCapture {
mut self: std::pin::Pin<&mut Self>, mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
match self.capture.poll_next_unpin(cx) { if let Some(e) = self.pending.pop_front() {
Poll::Ready(e) => { return Poll::Ready(Some(Ok(e)));
if let Some(Ok(( }
_,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key, state, .. })), // ready
))) = e 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);
{ {
self.update_pressed_keys(key, state); for &id in position_map.get(&pos).expect("position") {
self.pending.push_back((id, event));
}
} }
Poll::Ready(e) swap(&mut self.position_map, &mut position_map);
Poll::Ready(Some(Ok(self.pending.pop_front().expect("event"))))
} }
Poll::Pending => Poll::Pending,
} }
} }
} }
#[async_trait] #[async_trait]
trait Capture: Stream<Item = Result<(CaptureHandle, CaptureEvent), CaptureError>> + Unpin { trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + Unpin {
/// create a new client with the given id /// create a new client with the given id
async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError>; async fn create(&mut self, pos: Position) -> Result<(), CaptureError>;
/// destroy the client with the given id, if it exists /// destroy the client with the given id, if it exists
async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError>; async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError>;
/// release mouse /// release mouse
async fn release(&mut self) -> Result<(), CaptureError>; async fn release(&mut self) -> Result<(), CaptureError>;
@@ -204,14 +281,14 @@ trait Capture: Stream<Item = Result<(CaptureHandle, CaptureEvent), CaptureError>
async fn create_backend( async fn create_backend(
backend: Backend, backend: Backend,
) -> Result< ) -> Result<
Box<dyn Capture<Item = Result<(CaptureHandle, CaptureEvent), CaptureError>>>, Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError, CaptureCreationError,
> { > {
match backend { match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)), Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(wayland::WaylandInputCapture::new()?)), Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)), Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)] #[cfg(windows)]
@@ -225,7 +302,7 @@ async fn create_backend(
async fn create( async fn create(
backend: Option<Backend>, backend: Option<Backend>,
) -> Result< ) -> Result<
Box<dyn Capture<Item = Result<(CaptureHandle, CaptureEvent), CaptureError>>>, Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError, CaptureCreationError,
> { > {
if let Some(backend) = backend { if let Some(backend) = backend {
@@ -239,7 +316,7 @@ async fn create(
for backend in [ for backend in [
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal, Backend::InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell, Backend::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11, Backend::X11,

View File

@@ -1,6 +1,9 @@
use ashpd::{ use ashpd::{
desktop::{ desktop::{
input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones}, input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones,
},
Session, Session,
}, },
enumflags2::BitFlags, enumflags2::BitFlags,
@@ -8,14 +11,15 @@ use ashpd::{
use async_trait::async_trait; use async_trait::async_trait;
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use reis::{ use reis::{
ei, ei::{self, handshake::ContextType},
event::{DeviceCapability, EiEvent}, event::{Connection, DeviceCapability, EiEvent},
tokio::{EiConvertEventStream, EiEventStream}, tokio::EiConvertEventStream,
}; };
use std::{ use std::{
cell::Cell, cell::Cell,
collections::HashMap, collections::HashMap,
io, io,
num::NonZeroU32,
os::unix::net::UnixStream, os::unix::net::UnixStream,
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
@@ -32,15 +36,14 @@ use tokio::{
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use futures_core::Stream; use futures_core::Stream;
use once_cell::sync::Lazy;
use input_event::Event; use input_event::Event;
use crate::CaptureEvent; use crate::CaptureEvent;
use super::{ use super::{
error::{CaptureError, LibeiCaptureCreationError, ReisConvertEventStreamError}, error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, CaptureHandle, Position, Capture as LanMouseInputCapture, Position,
}; };
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that /* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
@@ -50,37 +53,21 @@ use super::{
/// events that necessitate restarting the capture session /// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent { enum LibeiNotifyEvent {
Create(CaptureHandle, Position), Create(Position),
Destroy(CaptureHandle), Destroy(Position),
} }
#[allow(dead_code)] #[allow(dead_code)]
pub struct LibeiInputCapture<'a> { pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>, input_capture: Pin<Box<InputCapture<'a>>>,
capture_task: JoinHandle<Result<(), CaptureError>>, capture_task: JoinHandle<Result<(), CaptureError>>,
event_rx: Receiver<(CaptureHandle, CaptureEvent)>, event_rx: Receiver<(Position, CaptureEvent)>,
notify_capture: Sender<LibeiNotifyEvent>, notify_capture: Sender<LibeiNotifyEvent>,
notify_release: Arc<Notify>, notify_release: Arc<Notify>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
terminated: bool, terminated: bool,
} }
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
});
/// returns (start pos, end pos), inclusive /// returns (start pos, end pos), inclusive
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) { fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
let (x, y) = (r.x_offset(), r.y_offset()); let (x, y) = (r.x_offset(), r.y_offset());
@@ -117,35 +104,37 @@ impl From<ICBarrier> for Barrier {
fn select_barriers( fn select_barriers(
zones: &Zones, zones: &Zones,
clients: &[(CaptureHandle, Position)], clients: &[Position],
next_barrier_id: &mut u32, next_barrier_id: &mut NonZeroU32,
) -> (Vec<ICBarrier>, HashMap<BarrierID, CaptureHandle>) { ) -> (Vec<ICBarrier>, HashMap<BarrierID, Position>) {
let mut client_for_barrier = HashMap::new(); let mut pos_for_barrier = HashMap::new();
let mut barriers: Vec<ICBarrier> = vec![]; let mut barriers: Vec<ICBarrier> = vec![];
for (handle, pos) in clients { for pos in clients {
let mut client_barriers = zones let mut client_barriers = zones
.regions() .regions()
.iter() .iter()
.map(|r| { .map(|r| {
let id = *next_barrier_id; let id = *next_barrier_id;
*next_barrier_id = id + 1; *next_barrier_id = next_barrier_id
.checked_add(1)
.expect("barrier id out of range");
let position = pos_to_barrier(r, *pos); let position = pos_to_barrier(r, *pos);
client_for_barrier.insert(id, *handle); pos_for_barrier.insert(id, *pos);
ICBarrier::new(id, position) ICBarrier::new(id, position)
}) })
.collect(); .collect();
barriers.append(&mut client_barriers); barriers.append(&mut client_barriers);
} }
(barriers, client_for_barrier) (barriers, pos_for_barrier)
} }
async fn update_barriers( async fn update_barriers(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>, session: &Session<'_, InputCapture<'_>>,
active_clients: &[(CaptureHandle, Position)], active_clients: &[Position],
next_barrier_id: &mut u32, next_barrier_id: &mut NonZeroU32,
) -> Result<(Vec<ICBarrier>, HashMap<BarrierID, CaptureHandle>), ashpd::Error> { ) -> Result<(Vec<ICBarrier>, HashMap<BarrierID, Position>), ashpd::Error> {
let zones = input_capture.zones(session).await?.response()?; let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}"); log::debug!("zones: {zones:?}");
@@ -168,7 +157,7 @@ async fn create_session<'a>(
log::debug!("creating input capture session"); log::debug!("creating input capture session");
input_capture input_capture
.create_session( .create_session(
&ashpd::WindowIdentifier::default(), None,
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
) )
.await .await
@@ -177,7 +166,7 @@ async fn create_session<'a>(
async fn connect_to_eis( async fn connect_to_eis(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>, session: &Session<'_, InputCapture<'_>>,
) -> Result<(ei::Context, EiConvertEventStream), CaptureError> { ) -> Result<(ei::Context, Connection, EiConvertEventStream), CaptureError> {
log::debug!("connect_to_eis"); log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?; let fd = input_capture.connect_to_eis(session).await?;
@@ -187,34 +176,27 @@ async fn connect_to_eis(
// create ei context // create ei context
let context = ei::Context::new(stream)?; let context = ei::Context::new(stream)?;
let mut event_stream = EiEventStream::new(context.clone())?; let (conn, event_stream) = context
let response = reis::tokio::ei_handshake( .handshake_tokio("de.feschber.LanMouse", ContextType::Receiver)
&mut event_stream, .await?;
"de.feschber.LanMouse",
ei::handshake::ContextType::Receiver,
&INTERFACES,
)
.await?;
let event_stream = EiConvertEventStream::new(event_stream, response.serial);
Ok((context, event_stream)) Ok((context, conn, event_stream))
} }
async fn libei_event_handler( async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream, mut ei_event_stream: EiConvertEventStream,
context: ei::Context, context: ei::Context,
event_tx: Sender<(CaptureHandle, CaptureEvent)>, event_tx: Sender<(Position, CaptureEvent)>,
release_session: Arc<Notify>, release_session: Arc<Notify>,
current_client: Rc<Cell<Option<CaptureHandle>>>, current_pos: Rc<Cell<Option<Position>>>,
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
loop { loop {
let ei_event = ei_event_stream let ei_event = ei_event_stream
.next() .next()
.await .await
.ok_or(CaptureError::EndOfStream)? .ok_or(CaptureError::EndOfStream)??;
.map_err(ReisConvertEventStreamError::from)?;
log::trace!("from ei: {ei_event:?}"); log::trace!("from ei: {ei_event:?}");
let client = current_client.get(); let client = current_pos.get();
handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?; handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?;
} }
} }
@@ -260,15 +242,15 @@ async fn do_capture(
mut capture_event: Receiver<LibeiNotifyEvent>, mut capture_event: Receiver<LibeiNotifyEvent>,
notify_release: Arc<Notify>, notify_release: Arc<Notify>,
session: Option<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>)>, session: Option<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>)>,
event_tx: Sender<(CaptureHandle, CaptureEvent)>, event_tx: Sender<(Position, CaptureEvent)>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
let mut session = session.map(|s| s.0); let mut session = session.map(|s| s.0);
/* safety: libei_task does not outlive Self */ /* safety: libei_task does not outlive Self */
let input_capture = unsafe { &*input_capture }; let input_capture = unsafe { &*input_capture };
let mut active_clients: Vec<(CaptureHandle, Position)> = vec![]; let mut active_clients: Vec<Position> = vec![];
let mut next_barrier_id = 1u32; let mut next_barrier_id = NonZeroU32::new(1).expect("id must be non-zero");
let mut zones_changed = input_capture.receive_zones_changed().await?; let mut zones_changed = input_capture.receive_zones_changed().await?;
@@ -341,8 +323,8 @@ async fn do_capture(
// update clients if requested // update clients if requested
if let Some(event) = capture_event_occured.take() { if let Some(event) = capture_event_occured.take() {
match event { match event {
LibeiNotifyEvent::Create(c, p) => active_clients.push((c, p)), LibeiNotifyEvent::Create(p) => active_clients.push(p),
LibeiNotifyEvent::Destroy(c) => active_clients.retain(|(h, _)| *h != c), LibeiNotifyEvent::Destroy(p) => active_clients.retain(|&pos| pos != p),
} }
} }
@@ -356,21 +338,21 @@ async fn do_capture(
async fn do_capture_session( async fn do_capture_session(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &mut Session<'_, InputCapture<'_>>, session: &mut Session<'_, InputCapture<'_>>,
event_tx: &Sender<(CaptureHandle, CaptureEvent)>, event_tx: &Sender<(Position, CaptureEvent)>,
active_clients: &[(CaptureHandle, Position)], active_clients: &[Position],
next_barrier_id: &mut u32, next_barrier_id: &mut NonZeroU32,
notify_release: &Notify, notify_release: &Notify,
cancel: (CancellationToken, CancellationToken), cancel: (CancellationToken, CancellationToken),
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
let (cancel_session, cancel_update) = cancel; let (cancel_session, cancel_update) = cancel;
// current client // current client
let current_client = Rc::new(Cell::new(None)); let current_pos = Rc::new(Cell::new(None));
// connect to eis server // connect to eis server
let (context, ei_event_stream) = connect_to_eis(input_capture, session).await?; let (context, _conn, ei_event_stream) = connect_to_eis(input_capture, session).await?;
// set barriers // set barriers
let (barriers, client_for_barrier_id) = let (barriers, pos_for_barrier_id) =
update_barriers(input_capture, session, active_clients, next_barrier_id).await?; update_barriers(input_capture, session, active_clients, next_barrier_id).await?;
log::debug!("enabling session"); log::debug!("enabling session");
@@ -382,7 +364,7 @@ async fn do_capture_session(
// async event task // async event task
let cancel_ei_handler = CancellationToken::new(); let cancel_ei_handler = CancellationToken::new();
let event_chan = event_tx.clone(); let event_chan = event_tx.clone();
let client = current_client.clone(); let pos = current_pos.clone();
let cancel_session_clone = cancel_session.clone(); let cancel_session_clone = cancel_session.clone();
let release_session_clone = release_session.clone(); let release_session_clone = release_session.clone();
let cancel_ei_handler_clone = cancel_ei_handler.clone(); let cancel_ei_handler_clone = cancel_ei_handler.clone();
@@ -393,7 +375,7 @@ async fn do_capture_session(
context, context,
event_chan, event_chan,
release_session_clone, release_session_clone,
client, pos,
) => { ) => {
log::debug!("libei exited: {r:?} cancelling session task"); log::debug!("libei exited: {r:?} cancelling session task");
cancel_session_clone.cancel(); cancel_session_clone.cancel();
@@ -415,17 +397,17 @@ async fn do_capture_session(
// get barrier id from activation // get barrier id from activation
let barrier_id = match activated.barrier_id() { let barrier_id = match activated.barrier_id() {
Some(bid) => bid, Some(ActivatedBarrier::Barrier(id)) => id,
// workaround for KDE plasma not reporting barrier ids // workaround for KDE plasma not reporting barrier ids
None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")), Some(ActivatedBarrier::UnknownBarrier) | None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")),
}; };
// find client corresponding to barrier // find client corresponding to barrier
let client = *client_for_barrier_id.get(&barrier_id).expect("invalid barrier id"); let pos = *pos_for_barrier_id.get(&barrier_id).expect("invalid barrier id");
current_client.replace(Some(client)); current_pos.replace(Some(pos));
// client entered => send event // client entered => send event
event_tx.send((client, CaptureEvent::Begin)).await.expect("no channel"); event_tx.send((pos, CaptureEvent::Begin)).await.expect("no channel");
tokio::select! { tokio::select! {
_ = notify_release.notified() => { /* capture release */ _ = notify_release.notified() => { /* capture release */
@@ -441,7 +423,7 @@ async fn do_capture_session(
}, },
} }
release_capture(input_capture, session, activated, client, active_clients).await?; release_capture(input_capture, session, activated, pos).await?;
} }
_ = notify_release.notified() => { /* capture release -> we are not capturing anyway, so ignore */ _ = notify_release.notified() => { /* capture release -> we are not capturing anyway, so ignore */
@@ -484,8 +466,7 @@ async fn release_capture<'a>(
input_capture: &InputCapture<'a>, input_capture: &InputCapture<'a>,
session: &Session<'a, InputCapture<'a>>, session: &Session<'a, InputCapture<'a>>,
activated: Activated, activated: Activated,
current_client: CaptureHandle, current_pos: Position,
active_clients: &[(CaptureHandle, Position)],
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
if let Some(activation_id) = activated.activation_id() { if let Some(activation_id) = activated.activation_id() {
log::debug!("releasing input capture {activation_id}"); log::debug!("releasing input capture {activation_id}");
@@ -494,13 +475,7 @@ async fn release_capture<'a>(
.cursor_position() .cursor_position()
.expect("compositor did not report cursor position!"); .expect("compositor did not report cursor position!");
log::debug!("client entered @ ({x}, {y})"); log::debug!("client entered @ ({x}, {y})");
let pos = active_clients let (dx, dy) = match current_pos {
.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 // offset cursor position to not enter again immediately
Position::Left => (1., 0.), Position::Left => (1., 0.),
Position::Right => (-1., 0.), Position::Right => (-1., 0.),
@@ -554,9 +529,9 @@ static ALL_CAPABILITIES: &[DeviceCapability] = &[
async fn handle_ei_event( async fn handle_ei_event(
ei_event: EiEvent, ei_event: EiEvent,
current_client: Option<CaptureHandle>, current_client: Option<Position>,
context: &ei::Context, context: &ei::Context,
event_tx: &Sender<(CaptureHandle, CaptureEvent)>, event_tx: &Sender<(Position, CaptureEvent)>,
release_session: &Notify, release_session: &Notify,
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
match ei_event { match ei_event {
@@ -575,9 +550,9 @@ async fn handle_ei_event(
return Err(CaptureError::Disconnected(format!("{:?}", d.reason))) return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
} }
_ => { _ => {
if let Some(handle) = current_client { if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) { for event in Event::from_ei_event(ei_event) {
event_tx.send((handle, CaptureEvent::Input(event))).await.expect("no channel"); event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
} }
} }
} }
@@ -587,18 +562,18 @@ async fn handle_ei_event(
#[async_trait] #[async_trait]
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> { impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
async fn create(&mut self, handle: CaptureHandle, pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self let _ = self
.notify_capture .notify_capture
.send(LibeiNotifyEvent::Create(handle, pos)) .send(LibeiNotifyEvent::Create(pos))
.await; .await;
Ok(()) Ok(())
} }
async fn destroy(&mut self, handle: CaptureHandle) -> Result<(), CaptureError> { async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self let _ = self
.notify_capture .notify_capture
.send(LibeiNotifyEvent::Destroy(handle)) .send(LibeiNotifyEvent::Destroy(pos))
.await; .await;
Ok(()) Ok(())
} }
@@ -629,7 +604,7 @@ impl<'a> Drop for LibeiInputCapture<'a> {
} }
impl<'a> Stream for LibeiInputCapture<'a> { impl<'a> Stream for LibeiInputCapture<'a> {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.capture_task.poll_unpin(cx) { match self.capture_task.poll_unpin(cx) {

View File

@@ -1,6 +1,4 @@
use super::{ use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, CaptureHandle, Position,
};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags; use bitflags::bitflags;
use core_foundation::base::{kCFAllocatorDefault, CFRelease}; use core_foundation::base::{kCFAllocatorDefault, CFRelease};
@@ -20,14 +18,14 @@ use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use libc::c_void; use libc::c_void;
use once_cell::unsync::Lazy; use once_cell::unsync::Lazy;
use std::collections::HashMap; use std::collections::HashSet;
use std::ffi::{c_char, CString}; use std::ffi::{c_char, CString};
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::{ready, Context, Poll}; use std::task::{ready, Context, Poll};
use std::thread::{self}; use std::thread::{self};
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::Mutex; use tokio::sync::{oneshot, Mutex};
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Bounds { struct Bounds {
@@ -39,44 +37,44 @@ struct Bounds {
#[derive(Debug)] #[derive(Debug)]
struct InputCaptureState { struct InputCaptureState {
client_for_pos: Lazy<HashMap<Position, CaptureHandle>>, active_clients: Lazy<HashSet<Position>>,
current_client: Option<(CaptureHandle, Position)>, current_pos: Option<Position>,
bounds: Bounds, bounds: Bounds,
} }
#[derive(Debug)] #[derive(Debug)]
enum ProducerEvent { enum ProducerEvent {
Release, Release,
Create(CaptureHandle, Position), Create(Position),
Destroy(CaptureHandle), Destroy(Position),
Grab((CaptureHandle, Position)), Grab(Position),
EventTapDisabled, EventTapDisabled,
} }
impl InputCaptureState { impl InputCaptureState {
fn new() -> Result<Self, MacosCaptureCreationError> { fn new() -> Result<Self, MacosCaptureCreationError> {
let mut res = Self { let mut res = Self {
client_for_pos: Lazy::new(HashMap::new), active_clients: Lazy::new(HashSet::new),
current_client: None, current_pos: None,
bounds: Bounds::default(), bounds: Bounds::default(),
}; };
res.update_bounds()?; res.update_bounds()?;
Ok(res) Ok(res)
} }
fn crossed(&mut self, event: &CGEvent) -> Option<(CaptureHandle, Position)> { fn crossed(&mut self, event: &CGEvent) -> Option<Position> {
let location = event.location(); let location = event.location();
let relative_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X); 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); let relative_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y);
for (position, client) in self.client_for_pos.iter() { for &position in self.active_clients.iter() {
if (position == &Position::Left && (location.x + relative_x) <= self.bounds.xmin) if (position == Position::Left && (location.x + relative_x) <= self.bounds.xmin)
|| (position == &Position::Right && (location.x + relative_x) >= self.bounds.xmax) || (position == Position::Right && (location.x + relative_x) >= self.bounds.xmax)
|| (position == &Position::Top && (location.y + relative_y) <= self.bounds.ymin) || (position == Position::Top && (location.y + relative_y) <= self.bounds.ymin)
|| (position == &Position::Bottom && (location.y + relative_y) >= self.bounds.ymax) || (position == Position::Bottom && (location.y + relative_y) >= self.bounds.ymax)
{ {
log::debug!("Crossed barrier into client: {client}, {position:?}"); log::debug!("Crossed barrier into position: {position:?}");
return Some((*client, *position)); return Some(position);
} }
} }
None None
@@ -102,7 +100,7 @@ impl InputCaptureState {
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a // 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 // random location when we exit the client
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> { fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
if let Some((_, pos)) = self.current_client { if let Some(pos) = self.current_pos {
let location = event.location(); let location = event.location();
let edge_offset = 1.0; let edge_offset = 1.0;
@@ -146,40 +144,31 @@ impl InputCaptureState {
log::debug!("handling event: {producer_event:?}"); log::debug!("handling event: {producer_event:?}");
match producer_event { match producer_event {
ProducerEvent::Release => { ProducerEvent::Release => {
if self.current_client.is_some() { if self.current_pos.is_some() {
CGDisplay::show_cursor(&CGDisplay::main()) CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?; .map_err(CaptureError::CoreGraphics)?;
self.current_client = None; self.current_pos = None;
} }
} }
ProducerEvent::Grab(client) => { ProducerEvent::Grab(pos) => {
if self.current_client.is_none() { if self.current_pos.is_none() {
CGDisplay::hide_cursor(&CGDisplay::main()) CGDisplay::hide_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?; .map_err(CaptureError::CoreGraphics)?;
self.current_client = Some(client); self.current_pos = Some(pos);
} }
} }
ProducerEvent::Create(c, p) => { ProducerEvent::Create(p) => {
self.client_for_pos.insert(p, c); self.active_clients.insert(p);
} }
ProducerEvent::Destroy(c) => { ProducerEvent::Destroy(p) => {
for pos in [ if let Some(current) = self.current_pos {
Position::Left, if current == p {
Position::Right, CGDisplay::show_cursor(&CGDisplay::main())
Position::Top, .map_err(CaptureError::CoreGraphics)?;
Position::Bottom, self.current_pos = None;
] { };
if let Some((current_c, _)) = self.current_client {
if current_c == c {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_client = None;
};
}
if self.client_for_pos.get(&pos).copied() == Some(c) {
self.client_for_pos.remove(&pos);
}
} }
self.active_clients.remove(&p);
} }
ProducerEvent::EventTapDisabled => return Err(CaptureError::EventTapDisabled), ProducerEvent::EventTapDisabled => return Err(CaptureError::EventTapDisabled),
}; };
@@ -333,12 +322,11 @@ fn get_events(
Ok(()) Ok(())
} }
fn event_tap_thread( fn create_event_tap<'a>(
client_state: Arc<Mutex<InputCaptureState>>, client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(CaptureHandle, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>, notify_tx: Sender<ProducerEvent>,
exit: tokio::sync::oneshot::Sender<Result<(), &'static str>>, event_tx: Sender<(Position, CaptureEvent)>,
) { ) -> Result<CGEventTap<'a>, MacosCaptureCreationError> {
let cg_events_of_interest: Vec<CGEventType> = vec![ let cg_events_of_interest: Vec<CGEventType> = vec![
CGEventType::LeftMouseDown, CGEventType::LeftMouseDown,
CGEventType::LeftMouseUp, CGEventType::LeftMouseUp,
@@ -356,15 +344,11 @@ fn event_tap_thread(
CGEventType::FlagsChanged, CGEventType::FlagsChanged,
]; ];
let tap = CGEventTap::new( let event_tap_callback =
CGEventTapLocation::Session, move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
cg_events_of_interest,
|_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
log::trace!("Got event from tap: {event_type:?}"); log::trace!("Got event from tap: {event_type:?}");
let mut state = client_state.blocking_lock(); let mut state = client_state.blocking_lock();
let mut client = None; let mut pos = None;
let mut res_events = vec![]; let mut res_events = vec![];
if matches!( if matches!(
@@ -380,8 +364,8 @@ fn event_tap_thread(
} }
// Are we in a client? // Are we in a client?
if let Some((current_client, _)) = state.current_client { if let Some(current_pos) = state.current_pos {
client = Some(current_client); pos = Some(current_pos);
get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| { get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| {
log::error!("Failed to get events: {e}"); log::error!("Failed to get events: {e}");
}); });
@@ -395,19 +379,19 @@ fn event_tap_thread(
} }
// Did we cross a barrier? // Did we cross a barrier?
else if matches!(event_type, CGEventType::MouseMoved) { else if matches!(event_type, CGEventType::MouseMoved) {
if let Some((new_client, pos)) = state.crossed(cg_ev) { if let Some(new_pos) = state.crossed(cg_ev) {
client = Some(new_client); pos = Some(new_pos);
res_events.push(CaptureEvent::Begin); res_events.push(CaptureEvent::Begin);
notify_tx notify_tx
.blocking_send(ProducerEvent::Grab((new_client, pos))) .blocking_send(ProducerEvent::Grab(new_pos))
.expect("Failed to send notification"); .expect("Failed to send notification");
} }
} }
if let Some(client) = client { if let Some(pos) = pos {
res_events.iter().for_each(|e| { res_events.iter().for_each(|e| {
event_tx event_tx
.blocking_send((client, *e)) .blocking_send((pos, *e))
.expect("Failed to send event"); .expect("Failed to send event");
}); });
// Returning None should stop the event from being processed // Returning None should stop the event from being processed
@@ -415,9 +399,16 @@ fn event_tap_thread(
cg_ev.set_type(CGEventType::Null); cg_ev.set_type(CGEventType::Null);
} }
Some(cg_ev.to_owned()) Some(cg_ev.to_owned())
}, };
let tap = CGEventTap::new(
CGEventTapLocation::Session,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
cg_events_of_interest,
event_tap_callback,
) )
.expect("Failed creating tap"); .map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap let tap_source: CFRunLoopSource = tap
.mach_port .mach_port
@@ -428,22 +419,43 @@ fn event_tap_thread(
CFRunLoop::get_current().add_source(&tap_source, kCFRunLoopCommonModes); 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(); CFRunLoop::run_current();
let _ = exit.send(Err("tap thread exited")); let _ = exit.send(Err("tap thread exited"));
} }
pub struct MacOSInputCapture { pub struct MacOSInputCapture {
event_rx: Receiver<(CaptureHandle, CaptureEvent)>, event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>, notify_tx: Sender<ProducerEvent>,
} }
impl MacOSInputCapture { impl MacOSInputCapture {
pub async fn new() -> Result<Self, MacosCaptureCreationError> { pub async fn new() -> Result<Self, MacosCaptureCreationError> {
let state = Arc::new(Mutex::new(InputCaptureState::new()?)); let state = Arc::new(Mutex::new(InputCaptureState::new()?));
let (event_tx, event_rx) = tokio::sync::mpsc::channel(32); let (event_tx, event_rx) = mpsc::channel(32);
let (notify_tx, mut notify_rx) = tokio::sync::mpsc::channel(32); let (notify_tx, mut notify_rx) = mpsc::channel(32);
let (tap_exit_tx, mut tap_exit_rx) = tokio::sync::oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (tap_exit_tx, mut tap_exit_rx) = oneshot::channel();
unsafe { unsafe {
configure_cf_settings()?; configure_cf_settings()?;
@@ -457,10 +469,14 @@ impl MacOSInputCapture {
event_tap_thread_state, event_tap_thread_state,
event_tx, event_tx,
event_tap_notify, event_tap_notify,
ready_tx,
tap_exit_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 { let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
loop { loop {
tokio::select! { tokio::select! {
@@ -491,21 +507,21 @@ impl MacOSInputCapture {
#[async_trait] #[async_trait]
impl Capture for MacOSInputCapture { impl Capture for MacOSInputCapture {
async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
log::debug!("creating client {id}, {pos}"); log::debug!("creating capture, {pos}");
let _ = notify_tx.send(ProducerEvent::Create(id, pos)).await; let _ = notify_tx.send(ProducerEvent::Create(pos)).await;
log::debug!("done !"); log::debug!("done !");
}); });
Ok(()) Ok(())
} }
async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> { async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
log::debug!("destroying client {id}"); log::debug!("destroying capture {pos}");
let _ = notify_tx.send(ProducerEvent::Destroy(id)).await; let _ = notify_tx.send(ProducerEvent::Destroy(pos)).await;
log::debug!("done !"); log::debug!("done !");
}); });
Ok(()) Ok(())
@@ -526,7 +542,7 @@ impl Capture for MacOSInputCapture {
} }
impl Stream for MacOSInputCapture { impl Stream for MacOSInputCapture {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) { match ready!(self.event_rx.poll_recv(cx)) {

View File

@@ -3,12 +3,12 @@ use core::task::{Context, Poll};
use futures::Stream; use futures::Stream;
use once_cell::unsync::Lazy; use once_cell::unsync::Lazy;
use std::collections::HashMap; use std::collections::HashSet;
use std::ptr::{addr_of, addr_of_mut}; use std::ptr::{addr_of, addr_of_mut};
use futures::executor::block_on; use futures::executor::block_on;
use std::default::Default; use std::default::Default;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{mpsc, Mutex}; use std::sync::{mpsc, Mutex};
use std::task::ready; use std::task::ready;
use std::{pin::Pin, thread}; use std::{pin::Pin, thread};
@@ -37,15 +37,15 @@ use input_event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
}; };
use super::{Capture, CaptureError, CaptureEvent, CaptureHandle, Position}; use super::{Capture, CaptureError, CaptureEvent, Position};
enum Request { enum Request {
Create(CaptureHandle, Position), Create(Position),
Destroy(CaptureHandle), Destroy(Position),
} }
pub struct WindowsInputCapture { pub struct WindowsInputCapture {
event_rx: Receiver<(CaptureHandle, CaptureEvent)>, event_rx: Receiver<(Position, CaptureEvent)>,
msg_thread: Option<std::thread::JoinHandle<()>>, msg_thread: Option<std::thread::JoinHandle<()>>,
} }
@@ -65,22 +65,22 @@ unsafe fn signal_message_thread(event_type: EventType) {
#[async_trait] #[async_trait]
impl Capture for WindowsInputCapture { impl Capture for WindowsInputCapture {
async fn create(&mut self, handle: CaptureHandle, pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
unsafe { unsafe {
{ {
let mut requests = REQUEST_BUFFER.lock().unwrap(); let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Create(handle, pos)); requests.push(Request::Create(pos));
} }
signal_message_thread(EventType::Request); signal_message_thread(EventType::Request);
} }
Ok(()) Ok(())
} }
async fn destroy(&mut self, handle: CaptureHandle) -> Result<(), CaptureError> { async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
unsafe { unsafe {
{ {
let mut requests = REQUEST_BUFFER.lock().unwrap(); let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Destroy(handle)); requests.push(Request::Destroy(pos));
} }
signal_message_thread(EventType::Request); signal_message_thread(EventType::Request);
} }
@@ -98,9 +98,9 @@ impl Capture for WindowsInputCapture {
} }
static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new()); static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new());
static mut ACTIVE_CLIENT: Option<CaptureHandle> = None; static mut ACTIVE_CLIENT: Option<Position> = None;
static mut CLIENT_FOR_POS: Lazy<HashMap<Position, CaptureHandle>> = Lazy::new(HashMap::new); static mut CLIENTS: Lazy<HashSet<Position>> = Lazy::new(HashSet::new);
static mut EVENT_TX: Option<Sender<(CaptureHandle, CaptureEvent)>> = None; static mut EVENT_TX: Option<Sender<(Position, CaptureEvent)>> = None;
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0); static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
unsafe fn set_event_tid(tid: u32) { unsafe fn set_event_tid(tid: u32) {
EVENT_THREAD_ID.store(tid, Ordering::SeqCst); EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
@@ -281,12 +281,12 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
}; };
/* check if a client is registered for the barrier */ /* check if a client is registered for the barrier */
let Some(client) = CLIENT_FOR_POS.get(&pos) else { if !CLIENTS.contains(&pos) {
return ret; return ret;
}; }
/* update active client and entry point */ /* update active client and entry point */
ACTIVE_CLIENT.replace(*client); ACTIVE_CLIENT.replace(pos);
ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos); ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
/* notify main thread */ /* notify main thread */
@@ -305,7 +305,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
} }
/* get active client if any */ /* get active client if any */
let Some(client) = ACTIVE_CLIENT else { let Some(pos) = ACTIVE_CLIENT else {
return LRESULT(1); return LRESULT(1);
}; };
@@ -313,7 +313,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
let Some(pointer_event) = to_mouse_event(wparam, lparam) else { let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1); return LRESULT(1);
}; };
let event = (client, CaptureEvent::Input(Event::Pointer(pointer_event))); let event = (pos, CaptureEvent::Input(Event::Pointer(pointer_event)));
/* notify mainthread (drop events if sending too fast) */ /* notify mainthread (drop events if sending too fast) */
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) { if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
@@ -493,6 +493,8 @@ fn get_msg() -> Option<MSG> {
} }
} }
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
fn message_thread(ready_tx: mpsc::Sender<()>) { fn message_thread(ready_tx: mpsc::Sender<()>) {
unsafe { unsafe {
set_event_tid(GetCurrentThreadId()); set_event_tid(GetCurrentThreadId());
@@ -513,9 +515,15 @@ fn message_thread(ready_tx: mpsc::Sender<()>) {
..Default::default() ..Default::default()
}; };
let ret = RegisterClassW(&window_class); if WINDOW_CLASS_REGISTERED
if ret == 0 { .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
panic!("RegisterClassW"); .is_ok()
{
/* register window class if not yet done so */
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
} }
/* window is used ro receive WM_DISPLAYCHANGE messages */ /* window is used ro receive WM_DISPLAYCHANGE messages */
@@ -575,23 +583,16 @@ fn message_thread(ready_tx: mpsc::Sender<()>) {
fn update_clients(request: Request) { fn update_clients(request: Request) {
match request { match request {
Request::Create(handle, pos) => { Request::Create(pos) => {
unsafe { CLIENT_FOR_POS.insert(pos, handle) }; unsafe { CLIENTS.insert(pos) };
} }
Request::Destroy(handle) => unsafe { Request::Destroy(pos) => unsafe {
for pos in [ if let Some(active_pos) = ACTIVE_CLIENT {
Position::Left, if pos == active_pos {
Position::Right, let _ = ACTIVE_CLIENT.take();
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);
}, },
} }
} }
@@ -614,7 +615,7 @@ impl WindowsInputCapture {
} }
impl Stream for WindowsInputCapture { impl Stream for WindowsInputCapture {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) { match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None), None => Poll::Ready(None),

View File

@@ -3,10 +3,7 @@ use std::task::Poll;
use async_trait::async_trait; use async_trait::async_trait;
use futures_core::Stream; use futures_core::Stream;
use super::{ use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, CaptureHandle,
Position,
};
pub struct X11InputCapture {} pub struct X11InputCapture {}
@@ -18,11 +15,11 @@ impl X11InputCapture {
#[async_trait] #[async_trait]
impl Capture for X11InputCapture { impl Capture for X11InputCapture {
async fn create(&mut self, _id: CaptureHandle, _pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(()) Ok(())
} }
async fn destroy(&mut self, _id: CaptureHandle) -> Result<(), CaptureError> { async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(()) Ok(())
} }
@@ -36,7 +33,7 @@ impl Capture for X11InputCapture {
} }
impl Stream for X11InputCapture { impl Stream for X11InputCapture {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>; type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next( fn poll_next(
self: std::pin::Pin<&mut Self>, self: std::pin::Pin<&mut Self>,

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "input-emulation" name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse" description = "cross-platform input emulation library used by lan-mouse"
version = "0.2.1" version = "0.3.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -10,8 +10,8 @@ repository = "https://github.com/feschber/lan-mouse"
async-trait = "0.1.80" async-trait = "0.1.80"
futures = "0.3.28" futures = "0.3.28"
log = "0.4.22" log = "0.4.22"
input-event = { path = "../input-event", version = "0.2.1" } input-event = { path = "../input-event", version = "0.3.0" }
thiserror = "1.0.61" thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [ tokio = { version = "1.32.0", features = [
"io-util", "io-util",
"io-std", "io-std",
@@ -38,13 +38,14 @@ wayland-protocols-misc = { version = "0.3.1", features = [
"client", "client",
], optional = true } ], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.9", default-features = false, features = [ ashpd = { version = "0.10", default-features = false, features = [
"tokio", "tokio",
], optional = true } ], optional = true }
reis = { version = "0.2", features = ["tokio"], optional = true } reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies] [target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] } bitflags = "2.6.0"
core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "0.4.0" keycode = "0.4.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
@@ -59,13 +60,13 @@ windows = { version = "0.58.0", features = [
] } ] }
[features] [features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei"] default = ["wlroots", "x11", "remote_desktop_portal", "libei"]
wayland = [ wlroots = [
"dep:wayland-client", "dep:wayland-client",
"dep:wayland-protocols", "dep:wayland-protocols",
"dep:wayland-protocols-wlr", "dep:wayland-protocols-wlr",
"dep:wayland-protocols-misc", "dep:wayland-protocols-misc",
] ]
x11 = ["dep:x11"] x11 = ["dep:x11"]
xdg_desktop_portal = ["dep:ashpd"] remote_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"] libei = ["dep:reis", "dep:ashpd"]

View File

@@ -6,53 +6,35 @@ pub enum InputEmulationError {
Emulate(#[from] EmulationError), Emulate(#[from] EmulationError),
} }
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
use ashpd::{desktop::ResponseError, Error::Response}; use ashpd::{desktop::ResponseError, Error::Response};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::EiConvertEventStreamError;
use std::io; use std::io;
use thiserror::Error; use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
ConnectError, DispatchError, ConnectError, DispatchError,
}; };
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::HandshakeError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("error in libei stream: {inner:?}")]
pub struct ReisConvertStreamError {
inner: EiConvertEventStreamError,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl From<EiConvertEventStreamError> for ReisConvertStreamError {
fn from(e: EiConvertEventStreamError) -> Self {
Self { inner: e }
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum EmulationError { pub enum EmulationError {
#[error("event stream closed")] #[error("event stream closed")]
EndOfStream, EndOfStream,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error flushing events: `{0}`")] #[error("libei error: `{0}`")]
Libei(#[from] reis::event::Error), Libei(#[from] reis::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("")]
LibeiConvertStream(#[from] ReisConvertStreamError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[error("wayland error: `{0}`")] #[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError), Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(all( #[cfg(all(
unix, unix,
any(feature = "xdg_desktop_portal", feature = "libei"), any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos") not(target_os = "macos")
))] ))]
#[error("xdg-desktop-portal: `{0}`")] #[error("xdg-desktop-portal: `{0}`")]
@@ -63,13 +45,13 @@ pub enum EmulationError {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum EmulationCreationError { pub enum EmulationCreationError {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wlroots backend: `{0}`")] #[error("wlroots backend: `{0}`")]
Wlroots(#[from] WlrootsEmulationCreationError), Wlroots(#[from] WlrootsEmulationCreationError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei backend: `{0}`")] #[error("libei backend: `{0}`")]
Libei(#[from] LibeiEmulationCreationError), Libei(#[from] LibeiEmulationCreationError),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[error("xdg-desktop-portal: `{0}`")] #[error("xdg-desktop-portal: `{0}`")]
Xdp(#[from] XdpEmulationCreationError), Xdp(#[from] XdpEmulationCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
@@ -97,7 +79,7 @@ impl EmulationCreationError {
) { ) {
return true; return true;
} }
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
if matches!( if matches!(
self, self,
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response( EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
@@ -110,7 +92,7 @@ impl EmulationCreationError {
} }
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum WlrootsEmulationCreationError { pub enum WlrootsEmulationCreationError {
#[error(transparent)] #[error(transparent)]
@@ -127,7 +109,7 @@ pub enum WlrootsEmulationCreationError {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("wayland protocol \"{protocol}\" not supported: {inner}")] #[error("wayland protocol \"{protocol}\" not supported: {inner}")]
pub struct WaylandBindError { pub struct WaylandBindError {
@@ -135,7 +117,7 @@ pub struct WaylandBindError {
protocol: &'static str, protocol: &'static str,
} }
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
impl WaylandBindError { impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self { pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol } Self { inner, protocol }
@@ -150,10 +132,10 @@ pub enum LibeiEmulationCreationError {
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
Handshake(#[from] HandshakeError), Reis(#[from] reis::Error),
} }
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum XdpEmulationCreationError { pub enum XdpEmulationCreationError {
#[error(transparent)] #[error(transparent)]

View File

@@ -14,10 +14,10 @@ mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11; mod x11;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
mod wlroots; mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
mod xdg_desktop_portal; mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
@@ -34,11 +34,11 @@ pub type EmulationHandle = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend { pub enum Backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Wlroots, Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei, Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Xdp, Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11, X11,
@@ -52,11 +52,11 @@ pub enum Backend {
impl Display for Backend { impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => write!(f, "wlroots"), Backend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => write!(f, "libei"), Backend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => write!(f, "xdg-desktop-portal"), Backend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"), Backend::X11 => write!(f, "X11"),
@@ -78,13 +78,13 @@ pub struct InputEmulation {
impl InputEmulation { impl InputEmulation {
async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> { async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
let emulation: Box<dyn Emulation> = match backend { let emulation: Box<dyn Emulation> = match backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?), Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => Box::new(libei::LibeiEmulation::new().await?), Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Box::new(x11::X11Emulation::new()?), Backend::X11 => Box::new(x11::X11Emulation::new()?),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?), Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
#[cfg(windows)] #[cfg(windows)]
Backend::Windows => Box::new(windows::WindowsEmulation::new()?), Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
@@ -109,11 +109,11 @@ impl InputEmulation {
} }
for backend in [ for backend in [
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots, Backend::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei, Backend::Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp, Backend::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11, Backend::X11,

View File

@@ -1,23 +1,18 @@
use futures::{future, StreamExt}; use futures::{future, StreamExt};
use once_cell::sync::Lazy;
use std::{ use std::{
collections::HashMap,
io, io,
os::{fd::OwnedFd, unix::net::UnixStream}, os::{fd::OwnedFd, unix::net::UnixStream},
sync::{ sync::{
atomic::{AtomicBool, AtomicU32, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock, Arc, Mutex, RwLock,
}, },
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use ashpd::{ use ashpd::desktop::{
desktop::{ remote_desktop::{DeviceType, RemoteDesktop},
remote_desktop::{DeviceType, RemoteDesktop}, PersistMode, Session,
PersistMode, Session,
},
WindowIdentifier,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -26,32 +21,16 @@ use reis::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard, self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
Pointer, Scroll, Pointer, Scroll,
}, },
event::{DeviceCapability, DeviceEvent, EiEvent, SeatEvent}, event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::{ei_handshake, EiConvertEventStream, EiEventStream}, tokio::EiConvertEventStream,
}; };
use input_event::{Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::{EmulationError, ReisConvertStreamError}; use crate::error::EmulationError;
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle}; use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("ei_connection", 1);
m.insert("ei_callback", 1);
m.insert("ei_pingpong", 1);
m.insert("ei_seat", 1);
m.insert("ei_device", 2);
m.insert("ei_pointer", 1);
m.insert("ei_pointer_absolute", 1);
m.insert("ei_scroll", 1);
m.insert("ei_button", 1);
m.insert("ei_keyboard", 1);
m.insert("ei_touchscreen", 1);
m
});
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct Devices { struct Devices {
pointer: Arc<RwLock<Option<(ei::Device, ei::Pointer)>>>, pointer: Arc<RwLock<Option<(ei::Device, ei::Pointer)>>>,
@@ -62,11 +41,11 @@ struct Devices {
pub(crate) struct LibeiEmulation<'a> { pub(crate) struct LibeiEmulation<'a> {
context: ei::Context, context: ei::Context,
conn: event::Connection,
devices: Devices, devices: Devices,
ei_task: JoinHandle<()>, ei_task: JoinHandle<()>,
error: Arc<Mutex<Option<EmulationError>>>, error: Arc<Mutex<Option<EmulationError>>>,
libei_error: Arc<AtomicBool>, libei_error: Arc<AtomicBool>,
serial: AtomicU32,
_remote_desktop: RemoteDesktop<'a>, _remote_desktop: RemoteDesktop<'a>,
session: Session<'a, RemoteDesktop<'a>>, session: Session<'a, RemoteDesktop<'a>>,
} }
@@ -89,10 +68,7 @@ async fn get_ei_fd<'a>(
.await?; .await?;
log::info!("requesting permission for input emulation"); log::info!("requesting permission for input emulation");
let _devices = remote_desktop let _devices = remote_desktop.start(&session, None).await?.response()?;
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
let fd = remote_desktop.connect_to_eis(&session).await?; let fd = remote_desktop.connect_to_eis(&session).await?;
Ok((remote_desktop, session, fd)) Ok((remote_desktop, session, fd))
@@ -104,20 +80,15 @@ impl<'a> LibeiEmulation<'a> {
let stream = UnixStream::from(eifd); let stream = UnixStream::from(eifd);
stream.set_nonblocking(true)?; stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?; let context = ei::Context::new(stream)?;
let mut events = EiEventStream::new(context.clone())?; let (conn, events) = context
let handshake = ei_handshake( .handshake_tokio("de.feschber.LanMouse", ContextType::Sender)
&mut events, .await?;
"de.feschber.LanMouse",
ContextType::Sender,
&INTERFACES,
)
.await?;
let events = EiConvertEventStream::new(events, handshake.serial);
let devices = Devices::default(); let devices = Devices::default();
let libei_error = Arc::new(AtomicBool::default()); let libei_error = Arc::new(AtomicBool::default());
let error = Arc::new(Mutex::new(None)); let error = Arc::new(Mutex::new(None));
let ei_handler = ei_task( let ei_handler = ei_task(
events, events,
conn.clone(),
context.clone(), context.clone(),
devices.clone(), devices.clone(),
libei_error.clone(), libei_error.clone(),
@@ -125,15 +96,13 @@ impl<'a> LibeiEmulation<'a> {
); );
let ei_task = tokio::task::spawn_local(ei_handler); let ei_task = tokio::task::spawn_local(ei_handler);
let serial = AtomicU32::new(handshake.serial);
Ok(Self { Ok(Self {
context, context,
conn,
devices, devices,
ei_task, ei_task,
error, error,
libei_error, libei_error,
serial,
_remote_desktop, _remote_desktop,
session, session,
}) })
@@ -169,7 +138,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
let pointer_device = self.devices.pointer.read().unwrap(); let pointer_device = self.devices.pointer.read().unwrap();
if let Some((d, p)) = pointer_device.as_ref() { if let Some((d, p)) = pointer_device.as_ref() {
p.motion_relative(dx as f32, dy as f32); p.motion_relative(dx as f32, dy as f32);
d.frame(self.serial.load(Ordering::SeqCst), now); d.frame(self.conn.serial(), now);
} }
} }
PointerEvent::Button { PointerEvent::Button {
@@ -186,7 +155,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
_ => ButtonState::Press, _ => ButtonState::Press,
}, },
); );
d.frame(self.serial.load(Ordering::SeqCst), now); d.frame(self.conn.serial(), now);
} }
} }
PointerEvent::Axis { PointerEvent::Axis {
@@ -200,7 +169,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
0 => s.scroll(0., value as f32), 0 => s.scroll(0., value as f32),
_ => s.scroll(value as f32, 0.), _ => s.scroll(value as f32, 0.),
} }
d.frame(self.serial.load(Ordering::SeqCst), now); d.frame(self.conn.serial(), now);
} }
} }
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
@@ -210,7 +179,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
0 => s.scroll_discrete(0, value), 0 => s.scroll_discrete(0, value),
_ => s.scroll_discrete(value, 0), _ => s.scroll_discrete(value, 0),
} }
d.frame(self.serial.load(Ordering::SeqCst), now); d.frame(self.conn.serial(), now);
} }
} }
}, },
@@ -229,7 +198,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
_ => KeyState::Press, _ => KeyState::Press,
}, },
); );
d.frame(self.serial.load(Ordering::SeqCst), now); d.frame(self.conn.serial(), now);
} }
} }
KeyboardEvent::Modifiers { .. } => {} KeyboardEvent::Modifiers { .. } => {}
@@ -252,6 +221,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
async fn ei_task( async fn ei_task(
mut events: EiConvertEventStream, mut events: EiConvertEventStream,
_conn: Connection,
context: ei::Context, context: ei::Context,
devices: Devices, devices: Devices,
libei_error: Arc<AtomicBool>, libei_error: Arc<AtomicBool>,
@@ -276,11 +246,7 @@ async fn ei_event_handler(
devices: &Devices, devices: &Devices,
) -> Result<(), EmulationError> { ) -> Result<(), EmulationError> {
loop { loop {
let event = events let event = events.next().await.ok_or(EmulationError::EndOfStream)??;
.next()
.await
.ok_or(EmulationError::EndOfStream)?
.map_err(ReisConvertStreamError::from)?;
const CAPABILITIES: &[DeviceCapability] = &[ const CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer, DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute, DeviceCapability::PointerAbsolute,

View File

@@ -1,15 +1,23 @@
use super::{error::EmulationError, Emulation, EmulationHandle}; use super::{error::EmulationError, Emulation, EmulationHandle};
use async_trait::async_trait; use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint}; use bitflags::bitflags;
use core_graphics::base::CGFloat;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayBounds, CGGetDisplaysWithRect, CGPoint, CGRect, CGSize,
};
use core_graphics::event::{ use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit, CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField,
ScrollEventUnit,
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{Event, KeyboardEvent, PointerEvent}; use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use std::cell::Cell;
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::task::AbortHandle; use tokio::{sync::Notify, task::JoinHandle};
use super::error::MacOSEmulationCreationError; use super::error::MacOSEmulationCreationError;
@@ -18,8 +26,10 @@ const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub(crate) struct MacOSEmulation { pub(crate) struct MacOSEmulation {
event_source: CGEventSource, event_source: CGEventSource,
repeat_task: Option<AbortHandle>, repeat_task: Option<JoinHandle<()>>,
button_state: ButtonState, button_state: ButtonState,
modifier_state: Rc<Cell<XMods>>,
notify_repeat_task: Arc<Notify>,
} }
struct ButtonState { struct ButtonState {
@@ -65,6 +75,8 @@ impl MacOSEmulation {
event_source, event_source,
button_state, button_state,
repeat_task: None, repeat_task: None,
notify_repeat_task: Arc::new(Notify::new()),
modifier_state: Rc::new(Cell::new(XMods::empty())),
}) })
} }
@@ -76,25 +88,40 @@ impl MacOSEmulation {
async fn spawn_repeat_task(&mut self, key: u16) { async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's // there can only be one repeating key and it's
// always the last to be pressed // always the last to be pressed
self.kill_repeat_task(); self.cancel_repeat_task().await;
let event_source = self.event_source.clone(); 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 repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await; let stop = tokio::select! {
loop { _ = tokio::time::sleep(DEFAULT_REPEAT_DELAY) => false,
key_event(event_source.clone(), key, 1); _ = notify.notified() => true,
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await; };
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.abort_handle()); self.repeat_task = Some(repeat_task);
} }
fn kill_repeat_task(&mut self) {
async fn cancel_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() { if let Some(task) = self.repeat_task.take() {
task.abort(); self.notify_repeat_task.notify_waiters();
let _ = task.await;
} }
} }
} }
fn key_event(event_source: CGEventSource, key: u16, state: u8) { fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) { let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e, Ok(e) => e,
Err(_) => { Err(_) => {
@@ -102,7 +129,92 @@ fn key_event(event_source: CGEventSource, key: u16, state: u8) {
return; return;
} }
}; };
event.set_flags(to_cgevent_flags(modifiers));
event.post(CGEventTapLocation::HID); 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] #[async_trait]
@@ -115,16 +227,6 @@ impl Emulation for MacOSEmulation {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => { PointerEvent::Motion { time: _, dx, dy } => {
// 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() { let mut mouse_location = match self.get_mouse_location() {
Some(l) => l, Some(l) => l,
None => { None => {
@@ -133,8 +235,11 @@ impl Emulation for MacOSEmulation {
} }
}; };
mouse_location.x = (mouse_location.x + dx).clamp(min_x, max_x - 1.); let (new_mouse_x, new_mouse_y) =
mouse_location.y = (mouse_location.y + dy).clamp(min_y, max_y - 1.); 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; let mut event_type = CGEventType::MouseMoved;
if self.button_state.left { if self.button_state.left {
@@ -279,11 +384,25 @@ impl Emulation for MacOSEmulation {
match state { match state {
// pressed // pressed
1 => self.spawn_repeat_task(code).await, 1 => self.spawn_repeat_task(code).await,
_ => self.kill_repeat_task(), _ => self.cancel_repeat_task().await,
} }
key_event(self.event_source.clone(), code, state) 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());
} }
KeyboardEvent::Modifiers { .. } => {}
}, },
} }
// FIXME // FIXME
@@ -296,3 +415,81 @@ impl Emulation for MacOSEmulation {
async fn terminate(&mut self) {} 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

@@ -4,7 +4,6 @@ use ashpd::{
PersistMode, Session, PersistMode, Session,
}, },
zbus::AsyncDrop, zbus::AsyncDrop,
WindowIdentifier,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -43,10 +42,7 @@ impl<'a> DesktopPortalEmulation<'a> {
.await?; .await?;
log::info!("requesting permission for input emulation"); log::info!("requesting permission for input emulation");
let _devices = proxy let _devices = proxy.start(&session, None).await?.response()?;
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
log::debug!("started session"); log::debug!("started session");
let session = session; let session = session;

View File

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

18
lan-mouse-cli/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[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

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

View File

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

18
lan-mouse-gtk/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[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" }

8
lan-mouse-gtk/build.rs Normal file
View File

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

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

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

View File

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

View File

@@ -2,16 +2,13 @@ mod client_object;
mod client_row; mod client_row;
mod window; mod window;
use std::{ use std::{env, process, str};
env,
io::{ErrorKind, Read},
process, str,
};
use crate::frontend::gtk::window::Window; use window::Window;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
use adw::Application; use adw::Application;
use endi::{Endian, ReadBytes};
use gtk::{ use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme, gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
}; };
@@ -19,8 +16,6 @@ use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;
use super::FrontendEvent;
pub fn run() -> glib::ExitCode { pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend"); log::debug!("running gtk frontend");
#[cfg(windows)] #[cfg(windows)]
@@ -65,15 +60,8 @@ fn load_icons() {
fn build_ui(app: &Application) { fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket"); log::debug!("connecting to lan-mouse-socket");
let mut rx = match super::wait_for_service() { let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
Ok(stream) => stream, Ok(conn) => conn,
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) => { Err(e) => {
log::error!("{e}"); log::error!("{e}");
process::exit(1); process::exit(1);
@@ -84,35 +72,18 @@ fn build_ui(app: &Application) {
let (sender, receiver) = async_channel::bounded(10); let (sender, receiver) = async_channel::bounded(10);
gio::spawn_blocking(move || { gio::spawn_blocking(move || {
match loop { while let Some(e) = frontend_rx.next_event() {
// read length match e {
let len = match rx.read_u64(Endian::Big) { Ok(e) => sender.send_blocking(e).unwrap(),
Ok(l) => l, Err(e) => {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), log::error!("{e}");
Err(e) => break Err(e), break;
}; }
// read payload
let mut buf = vec![0u8; len as usize];
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); let window = Window::new(app, frontend_tx);
glib::spawn_future_local(clone!( glib::spawn_future_local(clone!(
#[weak] #[weak]
@@ -121,6 +92,9 @@ fn build_ui(app: &Application) {
loop { loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1)); let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify { match notify {
FrontendEvent::Changed(handle) => {
window.request(FrontendRequest::GetState(handle));
}
FrontendEvent::Created(handle, client, state) => { FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state); window.new_client(handle, client, state);
} }

View File

@@ -1,16 +1,7 @@
mod imp; 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::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use endi::{Endian, WriteBytes};
use glib::{clone, Object}; use glib::{clone, Object};
use gtk::{ use gtk::{
gio, gio,
@@ -18,13 +9,12 @@ use gtk::{
ListBox, NoSelection, ListBox, NoSelection,
}; };
use crate::{ use lan_mouse_ipc::{
client::{ClientConfig, ClientHandle, ClientState, Position}, ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
config::DEFAULT_PORT, DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendRequest},
}; };
use super::client_row::ClientRow; use super::{client_object::ClientObject, client_row::ClientRow};
glib::wrapper! { glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>) pub struct Window(ObjectSubclass<imp::Window>)
@@ -34,13 +24,13 @@ glib::wrapper! {
} }
impl Window { impl Window {
pub(crate) fn new( pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
app: &adw::Application,
#[cfg(unix)] tx: UnixStream,
#[cfg(windows)] tx: TcpStream,
) -> Self {
let window: Self = Object::builder().property("application", app).build(); let window: Self = Object::builder().property("application", app).build();
window.imp().stream.borrow_mut().replace(tx); window
.imp()
.frontend_request_writer
.borrow_mut()
.replace(conn);
window window
} }
@@ -257,7 +247,11 @@ impl Window {
} }
pub fn request_client_state(&self, client: &ClientObject) { pub fn request_client_state(&self, client: &ClientObject) {
self.request(FrontendRequest::GetState(client.handle())); self.request_client_state_for(client.handle());
}
pub fn request_client_state_for(&self, handle: ClientHandle) {
self.request(FrontendRequest::GetState(handle));
} }
pub fn request_client_create(&self) { pub fn request_client_create(&self) {
@@ -292,16 +286,10 @@ impl Window {
self.request(FrontendRequest::Delete(client.handle())); self.request(FrontendRequest::Delete(client.handle()));
} }
pub fn request(&self, event: FrontendRequest) { pub fn request(&self, request: FrontendRequest) {
let json = serde_json::to_string(&event).unwrap(); let mut requester = self.imp().frontend_request_writer.borrow_mut();
log::debug!("requesting: {json}"); let requester = requester.as_mut().unwrap();
let mut stream = self.imp().stream.borrow_mut(); if let Err(e) = requester.request(request) {
let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes();
if let Err(e) = stream.write_u64(Endian::Big, bytes.len() as u64) {
log::error!("error sending message: {e}");
};
if let Err(e) = stream.write(bytes) {
log::error!("error sending message: {e}"); log::error!("error sending message: {e}");
}; };
} }

View File

@@ -1,17 +1,12 @@
use std::cell::{Cell, RefCell}; 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::subclass::prelude::*;
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay}; use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::glib::clone; use gtk::glib::clone;
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox}; use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
use crate::config::DEFAULT_PORT; use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")] #[template(resource = "/de/feschber/LanMouse/window.ui")]
@@ -41,10 +36,7 @@ pub struct Window {
#[template_child] #[template_child]
pub input_capture_button: TemplateChild<Button>, pub input_capture_button: TemplateChild<Button>,
pub clients: RefCell<Option<gio::ListStore>>, pub clients: RefCell<Option<gio::ListStore>>,
#[cfg(unix)] pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
pub stream: RefCell<Option<UnixStream>>,
#[cfg(windows)]
pub stream: RefCell<Option<TcpStream>>,
pub port: Cell<u16>, pub port: Cell<u16>,
pub capture_active: Cell<bool>, pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>, pub emulation_active: Cell<bool>,

16
lan-mouse-ipc/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[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

@@ -0,0 +1,89 @@
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

@@ -0,0 +1,104 @@
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
}

264
lan-mouse-ipc/src/lib.rs Normal file
View File

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

147
lan-mouse-ipc/src/listen.rs Normal file
View File

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

View File

@@ -44,6 +44,9 @@ rustPlatform.buildRustPackage {
postInstall = '' postInstall = ''
wrapProgram "$out/bin/lan-mouse" \ wrapProgram "$out/bin/lan-mouse" \
--set GDK_PIXBUF_MODULE_FILE ${pkgs.librsvg.out}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache --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; { meta = with lib; {

BIN
screenshots/dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
screenshots/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

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

View File

@@ -1,135 +1,8 @@
use std::{ use std::net::SocketAddr;
collections::HashSet,
fmt::Display,
net::{IpAddr, SocketAddr},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use slab::Slab; use slab::Slab;
use thiserror::Error;
use crate::config::DEFAULT_PORT; use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
use input_capture;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
#[default]
Left,
Right,
Top,
Bottom,
}
impl From<Position> for input_capture::Position {
fn from(position: Position) -> input_capture::Position {
match position {
Position::Left => input_capture::Position::Left,
Position::Right => input_capture::Position::Right,
Position::Top => input_capture::Position::Top,
Position::Bottom => input_capture::Position::Bottom,
}
}
}
#[derive(Debug, 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(Default)] #[derive(Default)]
pub struct ClientManager { pub struct ClientManager {

View File

@@ -1,22 +1,20 @@
use anyhow::Result;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::env::{self, VarError};
use std::env;
use std::fmt::Display; use std::fmt::Display;
use std::fs;
use std::net::IpAddr; use std::net::IpAddr;
use std::{error::Error, fs}; use std::{collections::HashSet, io};
use thiserror::Error;
use toml; use toml;
use crate::client::Position; use lan_mouse_ipc::{Position, DEFAULT_PORT};
use input_event::scancode::{ use input_event::scancode::{
self, self,
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift}, Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
}; };
pub const DEFAULT_PORT: u16 = 4242;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml { pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>, pub capture_backend: Option<CaptureBackend>,
@@ -42,7 +40,7 @@ pub struct TomlClient {
} }
impl ConfigToml { impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> { pub fn new(path: &str) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?; let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\""); log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?) Ok(toml::from_str::<_>(&config)?)
@@ -87,27 +85,33 @@ struct CliArgs {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend { pub enum CaptureBackend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[serde(rename = "input-capture-portal")]
InputCapturePortal, InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[serde(rename = "layer-shell")]
LayerShell, LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11, X11,
#[cfg(windows)] #[cfg(windows)]
#[serde(rename = "windows")]
Windows, Windows,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs, MacOs,
#[serde(rename = "dummy")]
Dummy, Dummy,
} }
impl Display for CaptureBackend { impl Display for CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"), CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => write!(f, "layer-shell"), CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => write!(f, "X11"), CaptureBackend::X11 => write!(f, "X11"),
#[cfg(windows)] #[cfg(windows)]
CaptureBackend::Windows => write!(f, "windows"), CaptureBackend::Windows => write!(f, "windows"),
@@ -121,11 +125,11 @@ impl Display for CaptureBackend {
impl From<CaptureBackend> for input_capture::Backend { impl From<CaptureBackend> for input_capture::Backend {
fn from(backend: CaptureBackend) -> Self { fn from(backend: CaptureBackend) -> Self {
match backend { match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal, CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => Self::LayerShell, CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => Self::X11, CaptureBackend::X11 => Self::X11,
#[cfg(windows)] #[cfg(windows)]
CaptureBackend::Windows => Self::Windows, CaptureBackend::Windows => Self::Windows,
@@ -138,31 +142,38 @@ impl From<CaptureBackend> for input_capture::Backend {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum EmulationBackend { pub enum EmulationBackend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[serde(rename = "wlroots")]
Wlroots, Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[serde(rename = "libei")]
Libei, Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[serde(rename = "xdp")]
Xdp, Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11, X11,
#[cfg(windows)] #[cfg(windows)]
#[serde(rename = "windows")]
Windows, Windows,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs, MacOs,
#[serde(rename = "dummy")]
Dummy, Dummy,
} }
impl From<EmulationBackend> for input_emulation::Backend { impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self { fn from(backend: EmulationBackend) -> Self {
match backend { match backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => Self::Wlroots, EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => Self::Libei, EmulationBackend::Libei => Self::Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => Self::Xdp, EmulationBackend::Xdp => Self::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => Self::X11, EmulationBackend::X11 => Self::X11,
#[cfg(windows)] #[cfg(windows)]
EmulationBackend::Windows => Self::Windows, EmulationBackend::Windows => Self::Windows,
@@ -176,13 +187,13 @@ impl From<EmulationBackend> for input_emulation::Backend {
impl Display for EmulationBackend { impl Display for EmulationBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => write!(f, "wlroots"), EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => write!(f, "libei"), EmulationBackend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))] #[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"), EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => write!(f, "X11"), EmulationBackend::X11 => write!(f, "X11"),
#[cfg(windows)] #[cfg(windows)]
EmulationBackend::Windows => write!(f, "windows"), EmulationBackend::Windows => write!(f, "windows"),
@@ -195,7 +206,9 @@ impl Display for EmulationBackend {
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
pub enum Frontend { pub enum Frontend {
#[serde(rename = "gtk")]
Gtk, Gtk,
#[serde(rename = "cli")]
Cli, Cli,
} }
@@ -231,11 +244,21 @@ pub struct ConfigClient {
pub enter_hook: Option<String>, 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] = const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt]; [KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config { impl Config {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, ConfigError> {
let args = CliArgs::parse(); let args = CliArgs::parse();
let config_file = "config.toml"; let config_file = "config.toml";
#[cfg(unix)] #[cfg(unix)]

View File

@@ -3,7 +3,8 @@ use std::net::IpAddr;
use hickory_resolver::{error::ResolveError, TokioAsyncResolver}; use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use crate::{client::ClientHandle, server::Server}; use crate::server::Server;
use lan_mouse_ipc::ClientHandle;
pub(crate) struct DnsResolver { pub(crate) struct DnsResolver {
resolver: TokioAsyncResolver, resolver: TokioAsyncResolver,

View File

@@ -1,30 +1,19 @@
use crate::config::Config; use crate::config::Config;
use anyhow::Result; use input_emulation::{InputEmulation, InputEmulationError};
use input_emulation::InputEmulation;
use input_event::{Event, PointerEvent}; use input_event::{Event, PointerEvent};
use std::f64::consts::PI; use std::f64::consts::PI;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::task::LocalSet;
pub fn run() -> Result<()> {
log::info!("running input emulation test");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
let config = Config::new()?;
runtime.block_on(LocalSet::new().run_until(input_emulation_test(config)))
}
const FREQUENCY_HZ: f64 = 1.0; const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0; const RADIUS: f64 = 100.0;
async fn input_emulation_test(config: Config) -> Result<()> { pub async fn run(config: Config) -> Result<(), InputEmulationError> {
log::info!("running input emulation test");
let backend = config.emulation_backend.map(|b| b.into()); let backend = config.emulation_backend.map(|b| b.into());
let mut emulation = InputEmulation::new(backend).await?; let mut emulation = InputEmulation::new(backend).await?;
emulation.create(0).await; emulation.create(0).await;
let start = Instant::now(); let start = Instant::now();
let mut offset = (0, 0); let mut offset = (0, 0);
loop { loop {

View File

@@ -1,307 +0,0 @@
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),
/// 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,
}
#[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,
}
}
}
#[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),
/// capture status
CaptureStatus(Status),
/// emulation status
EmulationStatus(Status),
}
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(&mut self, notify: FrontendEvent) {
// 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());
}
}
#[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

@@ -5,4 +5,3 @@ pub mod server;
pub mod capture_test; pub mod capture_test;
pub mod emulation_test; pub mod emulation_test;
pub mod frontend;

View File

@@ -1,11 +1,37 @@
use anyhow::Result;
use std::process::{self, Child, Command};
use env_logger::Env; use env_logger::Env;
use lan_mouse::{capture_test, config::Config, emulation_test, frontend, server::Server}; 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 tokio::task::LocalSet;
#[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),
}
pub fn main() { pub fn main() {
// init logging // init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info"); let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
@@ -17,32 +43,24 @@ pub fn main() {
} }
} }
pub fn start_service() -> Result<Child> { fn run() -> Result<(), LanMouseError> {
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 // parse config file + cli args
let config = Config::new()?; let config = Config::new()?;
log::debug!("{config:?}"); log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind); log::info!("release bind: {:?}", config.release_bind);
if config.test_capture { if config.test_capture {
capture_test::run()?; run_async(capture_test::run(config))?;
} else if config.test_emulation { } else if config.test_emulation {
emulation_test::run()?; run_async(emulation_test::run(config))?;
} else if config.daemon { } else if config.daemon {
// if daemon is specified we run the service // if daemon is specified we run the service
run_service(config)?; run_async(run_service(config))?;
} else { } else {
// otherwise start the service as a child process and // otherwise start the service as a child process and
// run a frontend // run a frontend
let mut service = start_service()?; let mut service = start_service()?;
frontend::run_frontend(&config)?; run_frontend(&config)?;
#[cfg(unix)] #[cfg(unix)]
{ {
// on unix we give the service a chance to terminate gracefully // on unix we give the service a chance to terminate gracefully
@@ -55,10 +73,14 @@ pub fn run() -> Result<()> {
service.kill()?; service.kill()?;
} }
anyhow::Ok(()) Ok(())
} }
fn run_service(config: Config) -> Result<()> { fn run_async<F, E>(f: F) -> Result<(), LanMouseError>
where
F: Future<Output = Result<(), E>>,
LanMouseError: From<E>,
{
// create single threaded tokio runtime // create single threaded tokio runtime
let runtime = tokio::runtime::Builder::new_current_thread() let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io() .enable_io()
@@ -66,15 +88,35 @@ fn run_service(config: Config) -> Result<()> {
.build()?; .build()?;
// run async event loop // run async event loop
runtime.block_on(LocalSet::new().run_until(async { Ok(runtime.block_on(LocalSet::new().run_until(f))?)
// run main loop }
log::info!("Press {:?} to release the mouse", config.release_bind);
let mut server = Server::new(config); fn start_service() -> Result<Child, io::Error> {
server.run().await?; let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("--daemon")
.spawn()?;
Ok(child)
}
log::debug!("service exiting"); async fn run_service(config: Config) -> Result<(), ServiceError> {
anyhow::Ok(()) 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()?;
}
};
Ok(()) Ok(())
} }

View File

@@ -1,30 +1,27 @@
use capture_task::CaptureRequest; use capture_task::CaptureRequest;
use emulation_task::EmulationRequest; use emulation_task::EmulationRequest;
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use local_channel::mpsc::{channel, Sender}; use local_channel::mpsc::{channel, Sender};
use log; use log;
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::{HashSet, VecDeque}, collections::{HashSet, VecDeque},
io::ErrorKind, io,
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
rc::Rc, rc::Rc,
}; };
use tokio::{io::ReadHalf, join, signal, sync::Notify, task::JoinHandle}; use thiserror::Error;
use tokio::{join, signal, sync::Notify};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{client::ClientManager, config::Config, dns::DnsResolver};
client::{ClientConfig, ClientHandle, ClientManager, ClientState, Position},
config::Config, use lan_mouse_ipc::{
dns::DnsResolver, AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
frontend::{self, FrontendEvent, FrontendListener, FrontendRequest, Status}, ListenerCreationError, Position, Status,
}; };
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
mod capture_task; mod capture_task;
mod emulation_task; mod emulation_task;
mod network_task; mod network_task;
@@ -41,6 +38,16 @@ enum State {
AwaitAck, AwaitAck,
} }
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Clone)] #[derive(Clone)]
pub struct Server { pub struct Server {
active_client: Rc<Cell<Option<ClientHandle>>>, active_client: Rc<Cell<Option<ClientHandle>>>,
@@ -51,7 +58,6 @@ pub struct Server {
notifies: Rc<Notifies>, notifies: Rc<Notifies>,
config: Rc<Config>, config: Rc<Config>,
pending_frontend_events: Rc<RefCell<VecDeque<FrontendEvent>>>, pending_frontend_events: Rc<RefCell<VecDeque<FrontendEvent>>>,
pending_dns_requests: Rc<RefCell<VecDeque<ClientHandle>>>,
capture_status: Rc<Cell<Status>>, capture_status: Rc<Cell<Status>>,
emulation_status: Rc<Cell<Status>>, emulation_status: Rc<Cell<Status>>,
} }
@@ -63,7 +69,6 @@ struct Notifies {
ping: Notify, ping: Notify,
port_changed: Notify, port_changed: Notify,
frontend_event_pending: Notify, frontend_event_pending: Notify,
dns_request_pending: Notify,
cancel: CancellationToken, cancel: CancellationToken,
} }
@@ -107,40 +112,32 @@ impl Server {
release_bind, release_bind,
notifies, notifies,
pending_frontend_events: Rc::new(RefCell::new(VecDeque::new())), pending_frontend_events: Rc::new(RefCell::new(VecDeque::new())),
pending_dns_requests: Rc::new(RefCell::new(VecDeque::new())),
capture_status: Default::default(), capture_status: Default::default(),
emulation_status: Default::default(), emulation_status: Default::default(),
} }
} }
pub async fn run(&mut self) -> anyhow::Result<()> { pub async fn run(&mut self) -> Result<(), ServiceError> {
// create frontend communication adapter, exit if already running // create frontend communication adapter, exit if already running
let mut frontend = match FrontendListener::new().await { let mut frontend = match AsyncFrontendListener::new().await {
Some(f) => f?, Ok(f) => f,
None => { Err(ListenerCreationError::AlreadyRunning) => {
log::info!("service already running, exiting"); log::info!("service already running, exiting");
return Ok(()); return Ok(());
} }
e => e?,
}; };
let (capture_tx, capture_rx) = channel(); /* requests for input capture */ let (capture_tx, capture_rx) = channel(); /* requests for input capture */
let (emulation_tx, emulation_rx) = channel(); /* emulation requests */ let (emulation_tx, emulation_rx) = channel(); /* emulation requests */
let (udp_recv_tx, udp_recv_rx) = channel(); /* udp receiver */ let (udp_recv_tx, udp_recv_rx) = channel(); /* udp receiver */
let (udp_send_tx, udp_send_rx) = channel(); /* udp sender */ let (udp_send_tx, udp_send_rx) = channel(); /* udp sender */
let (request_tx, mut request_rx) = channel(); /* frontend requests */
let (dns_tx, dns_rx) = channel(); /* dns requests */ let (dns_tx, dns_rx) = channel(); /* dns requests */
// udp task
let network = network_task::new(self.clone(), udp_recv_tx.clone(), udp_send_rx).await?; let network = network_task::new(self.clone(), udp_recv_tx.clone(), udp_send_rx).await?;
// input capture
let capture = capture_task::new(self.clone(), capture_rx, udp_send_tx.clone()); let capture = capture_task::new(self.clone(), capture_rx, udp_send_tx.clone());
// input emulation
let emulation = let emulation =
emulation_task::new(self.clone(), emulation_rx, udp_recv_rx, udp_send_tx.clone()); emulation_task::new(self.clone(), emulation_rx, udp_recv_rx, udp_send_tx.clone());
// create dns resolver
let resolver = DnsResolver::new(dns_rx)?; let resolver = DnsResolver::new(dns_rx)?;
let dns_task = tokio::task::spawn_local(resolver.run(self.clone())); let dns_task = tokio::task::spawn_local(resolver.run(self.clone()));
@@ -153,30 +150,22 @@ impl Server {
); );
for handle in self.active_clients() { for handle in self.active_clients() {
self.request_dns(handle); dns_tx.send(handle).expect("channel closed");
} }
log::info!("running service");
let mut join_handles = vec![];
loop { loop {
tokio::select! { tokio::select! {
stream = frontend.accept() => { request = frontend.next() => {
match stream { let request = match request {
Ok(s) => join_handles.push(handle_frontend_stream(self.notifies.cancel.clone(), s, request_tx.clone())), Some(Ok(r)) => r,
Err(e) => log::warn!("error accepting frontend connection: {e}"), Some(Err(e)) => {
log::error!("error receiving request: {e}");
continue;
}
None => break,
}; };
self.enumerate(); log::debug!("handle frontend request: {request:?}");
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status.get())); self.handle_request(&capture_tx.clone(), &emulation_tx.clone(), request, &dns_tx);
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status.get()));
self.notify_frontend(FrontendEvent::PortChanged(self.port.get(), None));
}
request = request_rx.recv() => {
let request = request.expect("channel closed");
log::debug!("received frontend request: {request:?}");
self.handle_request(&capture_tx.clone(), &emulation_tx.clone(), request).await;
log::debug!("handled frontend request");
} }
_ = self.notifies.frontend_event_pending.notified() => { _ = self.notifies.frontend_event_pending.notified() => {
while let Some(event) = { while let Some(event) = {
@@ -187,15 +176,6 @@ impl Server {
frontend.broadcast(event).await; frontend.broadcast(event).await;
} }
}, },
_ = self.notifies.dns_request_pending.notified() => {
while let Some(request) = {
/* need to drop borrow before next iteration! */
let request = self.pending_dns_requests.borrow_mut().pop_front();
request
} {
dns_tx.send(request).expect("channel closed");
}
}
_ = self.cancelled() => break, _ = self.cancelled() => break,
r = signal::ctrl_c() => { r = signal::ctrl_c() => {
r.expect("failed to wait for CTRL+C"); r.expect("failed to wait for CTRL+C");
@@ -207,7 +187,6 @@ impl Server {
log::info!("terminating service"); log::info!("terminating service");
self.cancel(); self.cancel();
futures::future::join_all(join_handles).await;
let _ = join!(capture, dns_task, emulation, network, ping); let _ = join!(capture, dns_task, emulation, network, ping);
Ok(()) Ok(())
@@ -231,6 +210,7 @@ impl Server {
} }
fn notify_capture(&self) { fn notify_capture(&self) {
log::info!("received capture enable request");
self.notifies.capture.notify_waiters() self.notifies.capture.notify_waiters()
} }
@@ -239,6 +219,7 @@ impl Server {
} }
fn notify_emulation(&self) { fn notify_emulation(&self) {
log::info!("received emulation enable request");
self.notifies.emulation.notify_waiters() self.notifies.emulation.notify_waiters()
} }
@@ -265,10 +246,7 @@ impl Server {
} }
pub(crate) fn client_updated(&self, handle: ClientHandle) { pub(crate) fn client_updated(&self, handle: ClientHandle) {
let state = self.client_manager.borrow().get(handle).cloned(); self.notify_frontend(FrontendEvent::Changed(handle));
if let Some((config, state)) = state {
self.notify_frontend(FrontendEvent::State(handle, config, state));
}
} }
fn active_clients(&self) -> Vec<ClientHandle> { fn active_clients(&self) -> Vec<ClientHandle> {
@@ -280,55 +258,49 @@ impl Server {
.collect() .collect()
} }
fn request_dns(&self, handle: ClientHandle) { fn handle_request(
self.pending_dns_requests.borrow_mut().push_back(handle);
self.notifies.dns_request_pending.notify_one();
}
async fn handle_request(
&self, &self,
capture: &Sender<CaptureRequest>, capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>, emulate: &Sender<EmulationRequest>,
event: FrontendRequest, event: FrontendRequest,
dns: &Sender<ClientHandle>,
) -> bool { ) -> bool {
log::debug!("frontend: {event:?}"); log::debug!("frontend: {event:?}");
match event { match event {
FrontendRequest::EnableCapture => { FrontendRequest::EnableCapture => self.notify_capture(),
log::info!("received capture enable request"); FrontendRequest::EnableEmulation => self.notify_emulation(),
self.notify_capture();
}
FrontendRequest::EnableEmulation => {
log::info!("received emulation enable request");
self.notify_emulation();
}
FrontendRequest::Create => { FrontendRequest::Create => {
let handle = self.add_client().await; self.add_client();
self.request_dns(handle);
} }
FrontendRequest::Activate(handle, active) => { FrontendRequest::Activate(handle, active) => {
if active { if active {
self.activate_client(capture, emulate, handle).await; self.activate_client(capture, emulate, handle);
} else { } else {
self.deactivate_client(capture, emulate, handle).await; self.deactivate_client(capture, emulate, handle);
} }
} }
FrontendRequest::ChangePort(port) => self.request_port_change(port), FrontendRequest::ChangePort(port) => self.request_port_change(port),
FrontendRequest::Delete(handle) => { FrontendRequest::Delete(handle) => {
self.remove_client(capture, emulate, handle).await; self.remove_client(capture, emulate, handle);
self.notify_frontend(FrontendEvent::Deleted(handle)); self.notify_frontend(FrontendEvent::Deleted(handle));
} }
FrontendRequest::Enumerate() => self.enumerate(), FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle), FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => { FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
self.update_fix_ips(handle, fix_ips); FrontendRequest::UpdateHostname(handle, host) => {
self.request_dns(handle); self.update_hostname(handle, host, dns)
} }
FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port), FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
FrontendRequest::UpdatePosition(handle, pos) => { FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, capture, emulate, pos).await; 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));
} }
FrontendRequest::ResolveDns(handle) => self.request_dns(handle),
}; };
false false
} }
@@ -343,7 +315,7 @@ impl Server {
self.notify_frontend(FrontendEvent::Enumerate(clients)); self.notify_frontend(FrontendEvent::Enumerate(clients));
} }
async fn add_client(&self) -> ClientHandle { fn add_client(&self) -> ClientHandle {
let handle = self.client_manager.borrow_mut().add_client(); let handle = self.client_manager.borrow_mut().add_client();
log::info!("added client {handle}"); log::info!("added client {handle}");
let (c, s) = self.client_manager.borrow().get(handle).unwrap().clone(); let (c, s) = self.client_manager.borrow().get(handle).unwrap().clone();
@@ -351,41 +323,40 @@ impl Server {
handle handle
} }
async fn deactivate_client( fn deactivate_client(
&self, &self,
capture: &Sender<CaptureRequest>, capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>, emulate: &Sender<EmulationRequest>,
handle: ClientHandle, handle: ClientHandle,
) { ) {
log::debug!("deactivating client {handle}");
match self.client_manager.borrow_mut().get_mut(handle) { match self.client_manager.borrow_mut().get_mut(handle) {
Some((_, s)) => s.active = false,
None => return, None => return,
Some((_, s)) if !s.active => return,
Some((_, s)) => s.active = false,
}; };
let _ = capture.send(CaptureRequest::Destroy(handle)); let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle)); let _ = emulate.send(EmulationRequest::Destroy(handle));
log::debug!("deactivating client {handle} done"); self.client_updated(handle);
log::info!("deactivated client {handle}");
} }
async fn activate_client( fn activate_client(
&self, &self,
capture: &Sender<CaptureRequest>, capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>, emulate: &Sender<EmulationRequest>,
handle: ClientHandle, handle: ClientHandle,
) { ) {
log::debug!("activating client");
/* deactivate potential other client at this position */ /* deactivate potential other client at this position */
let pos = match self.client_manager.borrow().get(handle) { let pos = match self.client_manager.borrow().get(handle) {
Some((client, _)) => client.pos,
None => return, None => return,
Some((_, s)) if s.active => return,
Some((client, _)) => client.pos,
}; };
let other = self.client_manager.borrow_mut().find_client(pos); let other = self.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other { if let Some(other) = other {
if other != handle { self.deactivate_client(capture, emulate, other);
self.deactivate_client(capture, emulate, other).await;
}
} }
/* activate the client */ /* activate the client */
@@ -396,12 +367,15 @@ impl Server {
}; };
/* notify emulation, capture and frontends */ /* notify emulation, capture and frontends */
let _ = capture.send(CaptureRequest::Create(handle, pos.into())); let _ = capture.send(CaptureRequest::Create(handle, to_capture_pos(pos)));
let _ = emulate.send(EmulationRequest::Create(handle)); let _ = emulate.send(EmulationRequest::Create(handle));
log::debug!("activating client {handle} done");
self.client_updated(handle);
log::info!("activated client {handle} ({pos})");
} }
async fn remove_client( fn remove_client(
&self, &self,
capture: &Sender<CaptureRequest>, capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>, emulate: &Sender<EmulationRequest>,
@@ -433,6 +407,7 @@ impl Server {
c.fix_ips = fix_ips; c.fix_ips = fix_ips;
}; };
self.update_ips(handle); self.update_ips(handle);
self.client_updated(handle);
} }
pub(crate) fn update_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) { pub(crate) fn update_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) {
@@ -440,6 +415,7 @@ impl Server {
s.dns_ips = dns_ips; s.dns_ips = dns_ips;
}; };
self.update_ips(handle); self.update_ips(handle);
self.client_updated(handle);
} }
fn update_ips(&self, handle: ClientHandle) { fn update_ips(&self, handle: ClientHandle) {
@@ -453,7 +429,12 @@ impl Server {
} }
} }
fn update_hostname(&self, handle: ClientHandle, hostname: Option<String>) { fn update_hostname(
&self,
handle: ClientHandle,
hostname: Option<String>,
dns: &Sender<ClientHandle>,
) {
let mut client_manager = self.client_manager.borrow_mut(); let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else { let Some((c, s)) = client_manager.get_mut(handle) else {
return; return;
@@ -462,10 +443,13 @@ impl Server {
// hostname changed // hostname changed
if c.hostname != hostname { if c.hostname != hostname {
c.hostname = hostname; c.hostname = hostname;
s.ips = HashSet::from_iter(c.fix_ips.iter().cloned());
s.active_addr = None; s.active_addr = None;
self.request_dns(handle); 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) { fn update_port(&self, handle: ClientHandle, port: u16) {
@@ -480,7 +464,7 @@ impl Server {
} }
} }
async fn update_pos( fn update_pos(
&self, &self,
handle: ClientHandle, handle: ClientHandle,
capture: &Sender<CaptureRequest>, capture: &Sender<CaptureRequest>,
@@ -494,18 +478,19 @@ impl Server {
}; };
let changed = c.pos != pos; let changed = c.pos != pos;
if changed {
log::info!("update pos {handle} {} -> {}", c.pos, pos);
}
c.pos = pos; c.pos = pos;
(changed, s.active) (changed, s.active)
}; };
// update state in event input emulator & input capture // update state in event input emulator & input capture
if changed { if changed {
self.deactivate_client(capture, emulate, handle);
if active { if active {
let _ = capture.send(CaptureRequest::Destroy(handle)); self.activate_client(capture, emulate, handle);
let _ = emulate.send(EmulationRequest::Destroy(handle));
} }
let _ = capture.send(CaptureRequest::Create(handle, pos.into()));
let _ = emulate.send(EmulationRequest::Create(handle));
} }
} }
@@ -538,7 +523,7 @@ impl Server {
self.client_updated(handle); self.client_updated(handle);
} }
pub(crate) fn get_hostname(&self, handle: u64) -> Option<String> { pub(crate) fn get_hostname(&self, handle: ClientHandle) -> Option<String> {
self.client_manager self.client_manager
.borrow_mut() .borrow_mut()
.get_mut(handle) .get_mut(handle)
@@ -554,12 +539,12 @@ impl Server {
self.state.replace(state); self.state.replace(state);
} }
fn set_active(&self, handle: Option<u64>) { fn set_active(&self, handle: Option<ClientHandle>) {
log::debug!("active client => {handle:?}"); log::debug!("active client => {handle:?}");
self.active_client.replace(handle); self.active_client.replace(handle);
} }
fn active_addr(&self, handle: u64) -> Option<SocketAddr> { fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
self.client_manager self.client_manager
.borrow() .borrow()
.get(handle) .get(handle)
@@ -567,41 +552,11 @@ impl Server {
} }
} }
async fn listen_frontend( fn to_capture_pos(pos: Position) -> input_capture::Position {
request_tx: Sender<FrontendRequest>, match pos {
#[cfg(unix)] mut stream: ReadHalf<UnixStream>, Position::Left => input_capture::Position::Left,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>, Position::Right => input_capture::Position::Right,
) { Position::Top => input_capture::Position::Top,
use std::io; Position::Bottom => input_capture::Position::Bottom,
loop {
let request = frontend::wait_for_request(&mut stream).await;
match request {
Ok(request) => {
let _ = request_tx.send(request);
}
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;
}
}
} }
} }
fn handle_frontend_stream(
cancel: CancellationToken,
#[cfg(unix)] stream: ReadHalf<UnixStream>,
#[cfg(windows)] stream: ReadHalf<TcpStream>,
request_tx: Sender<FrontendRequest>,
) -> JoinHandle<()> {
tokio::task::spawn_local(async move {
tokio::select! {
_ = listen_frontend(request_tx, stream) => {},
_ = cancel.cancelled() => {},
}
})
}

View File

@@ -9,7 +9,8 @@ use input_capture::{
self, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position, self, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
}; };
use crate::{client::ClientHandle, frontend::Status, server::State}; use crate::server::State;
use lan_mouse_ipc::{ClientHandle, Status};
use super::Server; use super::Server;
@@ -85,7 +86,7 @@ async fn do_capture(
) )
}); });
for (handle, pos) in clients { for (handle, pos) in clients {
capture.create(handle, pos.into()).await?; capture.create(handle, to_capture_pos(pos)).await?;
} }
loop { loop {
@@ -194,3 +195,12 @@ 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

@@ -4,12 +4,11 @@ use std::net::SocketAddr;
use lan_mouse_proto::ProtoEvent; use lan_mouse_proto::ProtoEvent;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use crate::{ use lan_mouse_ipc::ClientHandle;
client::{ClientHandle, ClientManager},
frontend::Status, use crate::{client::ClientManager, server::State};
server::State,
};
use input_emulation::{self, EmulationError, EmulationHandle, InputEmulation, InputEmulationError}; use input_emulation::{self, EmulationError, EmulationHandle, InputEmulation, InputEmulationError};
use lan_mouse_ipc::Status;
use super::{network_task::NetworkError, Server}; use super::{network_task::NetworkError, Server};

View File

@@ -4,7 +4,7 @@ use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::Sender; use local_channel::mpsc::Sender;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use crate::client::ClientHandle; use lan_mouse_ipc::ClientHandle;
use super::{capture_task::CaptureRequest, emulation_task::EmulationRequest, Server, State}; use super::{capture_task::CaptureRequest, emulation_task::EmulationRequest, Server, State};